diff --git a/crates/oxide_builtins/src/array.rs b/crates/oxide_builtins/src/array.rs index c22b09bd..e70b7ba5 100644 --- a/crates/oxide_builtins/src/array.rs +++ b/crates/oxide_builtins/src/array.rs @@ -619,7 +619,7 @@ fn array_from_set_length(vm: &mut H, a: *mut JsObject, is_array: bool let length_key = vm.new_string("length"); let length_si = vm.property_key_si(length_key); let a_val = JsValue::from_js_object(a); - match vm.ordinary_set(a_obj, length_si, JsValue::int(len as i32), a_val) { + match vm.ordinary_set(a_obj, length_si, JsValue::int(len as i32), a_val, true) { Ok(()) => Ok(()), Err(err) => Err(from_engine_error(vm, &err)), } @@ -731,7 +731,7 @@ pub fn array_slice(vm: &mut H, args: &[u8]) -> NativeResult { let a_val = JsValue::from_js_object(a_ptr); 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) { + if let Err(err) = vm.ordinary_set(unsafe { &mut *a_ptr }, length_si, js_array_index(out_idx), a_val, true) { return NativeResult::Err(from_engine_error(vm, &err)); } NativeResult::Ok(JsValue::from_js_object(a_ptr)) @@ -1843,7 +1843,7 @@ pub fn array_iterator_next(vm: &mut H, args: &[u8]) -> NativeResult { } _ => element, }; - if let Err(msg) = vm.ordinary_set(iter, index_si, JsValue::int(index + 1), this_val) { + if let Err(msg) = vm.ordinary_set(iter, index_si, JsValue::int(index + 1), this_val, true) { return NativeResult::Err(crate::error::create_from_text(vm, &msg)); } return NativeResult::Ok(crate::iterator::make_iter_result(vm, value, false)); @@ -1876,7 +1876,7 @@ pub fn array_iterator_next(vm: &mut H, args: &[u8]) -> NativeResult { } _ => element, }; - if let Err(msg) = vm.ordinary_set(iter, index_si, JsValue::int(index + 1), this_val) { + if let Err(msg) = vm.ordinary_set(iter, index_si, JsValue::int(index + 1), this_val, true) { return NativeResult::Err(crate::error::create_from_text(vm, &msg)); } NativeResult::Ok(crate::iterator::make_iter_result(vm, value, false)) diff --git a/crates/oxide_builtins/src/iterator.rs b/crates/oxide_builtins/src/iterator.rs index 5ce94114..538cd30d 100644 --- a/crates/oxide_builtins/src/iterator.rs +++ b/crates/oxide_builtins/src/iterator.rs @@ -114,7 +114,7 @@ fn iterator_setter_ignore_proto_props(vm: &mut H, args: &[u8], key: u Err(err) => NativeResult::Err(crate::error::create_type_error(vm, &err)), } } else { - match vm.ordinary_set(this_obj, key, val, this_val) { + match vm.ordinary_set(this_obj, key, val, this_val, true) { Ok(()) => NativeResult::Ok(JsValue::undefined()), Err(err) => NativeResult::Err(crate::error::create_type_error(vm, &err)), } diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 403ddf8d..6cc66a91 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -496,7 +496,7 @@ pub fn object_assign(vm: &mut H, args: &[u8]) -> NativeResult { for (si, val) in all_assignments { let promoted = vm.promote_if_needed_for_write_ptr(target_ptr, val); // Set(to, key, value, true):receiver 为目标对象(目标同名 setter 的 this 指向 target)。 - if let Err(err) = vm.ordinary_set(target, si, promoted, target_val) { + if let Err(err) = vm.ordinary_set(target, si, promoted, target_val, true) { return NativeResult::Err(crate::error::create_type_error(vm, &err)); } } @@ -1089,7 +1089,7 @@ pub fn object_from_entries(vm: &mut H, args: &[u8]) -> NativeResult { // ToPropertyKey 语义建键:int/规范数字串/symbol 统一映射,避免数字键分裂。 let si = vm.property_key_si(key_val); let promoted = vm.promote_if_needed_for_write_ptr(obj, value_val); - let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val); + let _ = vm.ordinary_set(unsafe { &mut *obj }, si, promoted, target_val, true); } NativeResult::Ok(target_val) } @@ -1483,7 +1483,7 @@ pub fn object_group_by(vm: &mut H, args: &[u8]) -> NativeResult { let new_arr_val = JsValue::from_js_object(arr); let result_ref_mut = unsafe { &mut *result }; let promoted = vm.promote_if_needed_for_write_ptr(result, new_arr_val); - let _ = vm.ordinary_set(result_ref_mut, key_si, promoted, result_val); + let _ = vm.ordinary_set(result_ref_mut, key_si, promoted, result_val, true); new_arr_val }; // push element 到分组数组(写入前 promote,与 Array.prototype.push 同款)。 diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index 4cf28edf..796667f4 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -222,7 +222,7 @@ pub fn reflect_set(vm: &mut H, args: &[u8]) -> NativeResult { if !target.is_extensible() && vm.get_own_property_slot(target, key_si).is_none() { return NativeResult::Ok(JsValue::bool(false)); } - NativeResult::Ok(JsValue::bool(vm.ordinary_set(target, key_si, value, receiver).is_ok())) + NativeResult::Ok(JsValue::bool(vm.ordinary_set(target, key_si, value, receiver, true).is_ok())) } /// `Reflect.setPrototypeOf(target, proto)`:设置 prototype(对象或 null)。 diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index c7c7bd78..3e6fa973 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -151,8 +151,10 @@ pub trait VmHost { // 属性访问 fn ordinary_get(&mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue) -> Result; + /// `strict` 为写方严格模式:写失败时严格抛错,sloppy 静默 no-op。内置调用方 + /// 一律传 true(内置写语义恒抛错,与调用上下文模式无关)。 fn ordinary_set( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, ) -> Result<(), String>; // 属性定义 diff --git a/crates/oxide_vm/src/dispatch/member.rs b/crates/oxide_vm/src/dispatch/member.rs index a85b5ddd..1c8f19c5 100644 --- a/crates/oxide_vm/src/dispatch/member.rs +++ b/crates/oxide_vm/src/dispatch/member.rs @@ -160,7 +160,7 @@ impl Vm { let prop_val = self.ordinary_get(obj, prop_name_si, receiver)?; let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n + 1.0); - self.ordinary_set(obj, prop_name_si, new_val, receiver)?; + self.ordinary_set(obj, prop_name_si, new_val, receiver, self.current_strict())?; self.regs[b] = new_val; Ok(()) } @@ -176,7 +176,7 @@ impl Vm { let prop_val = self.ordinary_get(obj, prop_name_si, receiver)?; let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n - 1.0); - self.ordinary_set(obj, prop_name_si, new_val, receiver)?; + self.ordinary_set(obj, prop_name_si, new_val, receiver, self.current_strict())?; self.regs[b] = new_val; Ok(()) } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 486a46f3..7a9202a2 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -435,7 +435,7 @@ impl Vm { } if obj.has_prop_meta() { - self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; + self.ordinary_set_dispatch(obj, prop_name_si, value, receiver, self.current_strict())?; return Ok(()); } @@ -451,10 +451,10 @@ impl Vm { // 写新属性(shape 转换):数组 length / 整数索引键仍走完整 // ordinary_set_dispatch 语义,其余走 CreateDataProperty 快路径。 if self.named_prop_create_needs_ordinary_set(obj, prop_name_si) { - self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; + self.ordinary_set_dispatch(obj, prop_name_si, value, receiver, self.current_strict())?; return Ok(()); } - self.create_named_prop_fast(obj, prop_name_si, value, receiver, ic_pc, true)?; + self.create_named_prop_fast(obj, prop_name_si, value, receiver, ic_pc, true, self.current_strict())?; } Ok(()) @@ -496,7 +496,7 @@ impl Vm { } let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); let obj = unsafe { &mut *obj_ptr }; - self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd])?; + self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd], self.current_strict())?; Ok(()) } @@ -593,7 +593,7 @@ impl Vm { } let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); let obj = unsafe { &mut *obj_ptr }; - self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd])?; + self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd], self.current_strict())?; Ok(()) } @@ -616,7 +616,7 @@ impl Vm { // 直写绕过冻结语义。 if obj.has_prop_meta() { let si = make_int_key(idx); - return self.ordinary_set_dispatch(obj, si, value, self.regs[rd]); + return self.ordinary_set_dispatch(obj, si, value, self.regs[rd], self.current_strict()); } // 无 meta 快路径:不可扩展对象禁止新增元素(越界下标);已有元素直写不受限 // (无 meta 即默认可写)。 diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 4f6775fd..256d7d3e 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -1012,6 +1012,7 @@ mod tests { saved_new_target: JsValue::from_js_object(child_session), callee: JsValue::from_js_object(child_session), construct_result_reg: None, + strict: false, constructed_this: Some(JsValue::from_js_object(child_session)), is_derived_constructor: false, super_called: false, diff --git a/crates/oxide_vm/src/suspended.rs b/crates/oxide_vm/src/suspended.rs index 62564e35..d950607c 100644 --- a/crates/oxide_vm/src/suspended.rs +++ b/crates/oxide_vm/src/suspended.rs @@ -289,6 +289,7 @@ impl SuspendedFrame { constructed_this: f.constructed_this.map(&mut rewrite), is_derived_constructor: f.is_derived_constructor, super_called: f.super_called, + strict: f.strict, continuation: f.continuation, }), spill_stack: self.spill_stack.iter().copied().map(&mut rewrite).collect(), @@ -365,6 +366,7 @@ mod tests { constructed_this: Some(tag), is_derived_constructor: false, super_called: false, + strict: false, continuation: FrameContinuation::None, } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 084e4276..1bbeb54a 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -220,6 +220,9 @@ pub struct CallFrame { /// 旧方案——多层继承时中间 derived 帧由 SUPER_CALL 以构造 this 压入, /// regs[254] 恒非 undefined,值判定三重失效。 pub super_called: bool, + /// 本帧函数的严格模式标志(来源:模块编译产物 `is_strict`)。 + /// 属性写失败时据此分派:严格模式抛 TypeError,sloppy 静默 no-op。 + pub strict: bool, pub continuation: FrameContinuation, } @@ -372,6 +375,11 @@ pub(crate) struct InlineSyncState { pub(crate) spill_stack: Vec, pub(crate) cell_stack: Vec>, pub(crate) inline_callee: Option, + /// inline 目标函数的严格模式标志(内联执行期间写路径的 strict/sloppy 判定 + /// 来源;嵌套内联时随本快照保存/恢复)。 + pub(crate) inline_strict: bool, + /// inline 起始时 `frames.len()` 基线(嵌套内联时随本快照保存/恢复)。 + pub(crate) inline_frames_base: usize, pub(crate) inline_args_base: u32, pub(crate) inline_args_count: u16, pub(crate) accessor_frame_target_reg: Option, @@ -474,6 +482,16 @@ pub struct Vm { /// frames 为空(inline 隔离状态)时由此取闭包 upvalues。嵌套 inline 由 /// InlineSyncState 保存/恢复。 pub(crate) inline_callee: Option, + /// inline 目标函数的严格模式标志(与 `inline_callee` 同生命周期, + /// InlineSyncState 保存/恢复):内联执行期间帧表被隔离,写路径的 + /// strict/sloppy 判定取此值而非帧标志。 + pub(crate) inline_strict: bool, + /// inline 起始时 `frames.len()` 基线(随 InlineSyncState 保存/恢复): + /// 内联执行中新压的帧(CALL/accessor)越过该基线,其严格性归帧栈顶。 + pub(crate) inline_frames_base: usize, + /// 本次 run 顶层脚本的严格模式标志(`module.is_strict`,run 时填充): + /// 无帧且无 inline(顶层脚本赋值)时写路径的 strict/sloppy 判定来源。 + pub(crate) top_level_strict: bool, /// inline 同步调用寄存器窗口缓冲池:`save_inline_state` 取出复用、 /// `restore_inline_state` 归还。热回调循环内 save/restore 反复使用同一块 /// 缓冲,只在嵌套(池已被外层取走)时新分配。 @@ -1327,6 +1345,27 @@ impl Vm { .max(1) } + /// 当前执行上下文的严格模式标志:属性写失败路径据此分派(严格抛 + /// TypeError,sloppy 静默 no-op)。 + /// + /// # 边界与前提 + /// - inline 执行期间帧表可能非空(调用方帧隔离在外),须以 + /// `inline_frames_base` 为基线区分"inline 前已有帧"与"inline 内新压帧" + /// (CALL/accessor 帧严格性归目标函数,最内层执行上下文即帧栈顶)。 + /// - 无帧且无 inline 时为顶层脚本执行,取 `top_level_strict`。 + pub(crate) fn current_strict(&self) -> bool { + if self.inline_callee.is_some() { + if self.frames.len() > self.inline_frames_base { + return self.frames.last().unwrap().strict; + } + return self.inline_strict; + } + if let Some(frame) = self.frames.last() { + return frame.strict; + } + self.top_level_strict + } + #[expect(clippy::too_many_arguments)] pub(crate) fn push_bytecode_frame( &mut self, callee: JsValue, this_value: JsValue, args: FrameArgs, construct_result_reg: Option, @@ -1436,6 +1475,7 @@ impl Vm { constructed_this, is_derived_constructor: obj.is_derived_constructor(), super_called: false, + strict: sub_is_strict, continuation, }); @@ -2207,9 +2247,9 @@ impl oxide_runtime_api::VmHost for Vm { self.ordinary_get(obj, prop_name_si, receiver) } fn ordinary_set( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, ) -> Result<(), String> { - self.ordinary_set(obj, prop_name_si, val, receiver) + self.ordinary_set(obj, prop_name_si, val, receiver, strict) } fn define_data_property( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, @@ -2528,6 +2568,7 @@ mod tests { saved_new_target: JsValue::undefined(), callee: JsValue::undefined(), construct_result_reg: None, + strict: false, constructed_this: None, is_derived_constructor: false, super_called: false, @@ -2682,7 +2723,7 @@ mod tests { let x_si = vm.kernel_core.perm_interner().intern("x").0; let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; - vm.ordinary_set(obj, x_si, JsValue::int(9), obj_val).expect("setter"); + vm.ordinary_set(obj, x_si, JsValue::int(9), obj_val, true).expect("setter"); let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let obj = unsafe { &*obj_val.as_js_object_ptr() }; @@ -2720,7 +2761,7 @@ mod tests { let x_si = vm.kernel_core.perm_interner().intern("x").0; let child = unsafe { &mut *child_val.as_js_object_ptr() }; - vm.ordinary_set(child, x_si, JsValue::int(12), child_val).expect("setter"); + vm.ordinary_set(child, x_si, JsValue::int(12), child_val, true).expect("setter"); let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let child = unsafe { &*child_val.as_js_object_ptr() }; @@ -2744,7 +2785,7 @@ mod tests { let x_si = vm.kernel_core.perm_interner().intern("x").0; let child = unsafe { &mut *child_val.as_js_object_ptr() }; - vm.ordinary_set(child, x_si, JsValue::int(15), child_val).expect("setter"); + vm.ordinary_set(child, x_si, JsValue::int(15), child_val, true).expect("setter"); let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let child = unsafe { &*child_val.as_js_object_ptr() }; @@ -2761,7 +2802,7 @@ mod tests { let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; assert!(!obj.has_prop_meta()); assert_eq!(vm.ordinary_get(obj, x_si, obj_val).expect("get"), JsValue::int(1)); - vm.ordinary_set(obj, x_si, JsValue::int(2), obj_val).expect("set"); + vm.ordinary_set(obj, x_si, JsValue::int(2), obj_val, true).expect("set"); assert_eq!(vm.ordinary_get(obj, x_si, obj_val).expect("get"), JsValue::int(2)); } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index bc230ba2..4b163e71 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -152,22 +152,25 @@ impl Vm { None } + /// `strict` 为写方(执行赋值的那个函数/脚本)的严格模式:写失败时严格抛 + /// TypeError,sloppy 静默 no-op(ECMA-262 OrdinarySetOwnProperty 失败分支)。 pub(crate) fn ordinary_set( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + &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) + self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict) } /// 分发期入口:调用方(dispatch_set_prop 等)已对值做过 promote。 pub(crate) fn ordinary_set_dispatch( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, ) -> Result<(), String> { - self.ordinary_set_inner(obj, prop_name_si, val, receiver, true) + self.ordinary_set_inner(obj, prop_name_si, val, receiver, true, strict) } fn ordinary_set_inner( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, + strict: bool, ) -> Result<(), String> { vm_trace!( "ordinary_set_inner: shape={} prop_si={} frame_push={}", @@ -180,7 +183,15 @@ impl Vm { if obj.is_typed_array_obj() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { if !std::ptr::eq(receiver.as_js_object_ptr(), obj as *mut JsObject) { - return self.set_to_receiver(obj, prop_name_si, val, receiver, index as usize, use_frame_push); + return self.set_to_receiver( + obj, + prop_name_si, + val, + receiver, + index as usize, + use_frame_push, + strict, + ); } return oxide_builtins::typed_array::typed_array_element_set(self, obj, index, val); } @@ -190,8 +201,8 @@ impl Vm { // prop_count/迭代/内置方法看到的长度不一致。 let length_si = self.length_si; if obj.is_array() && prop_name_si == length_si { - // 冻结数组的 length 属性不可写(writable=false),赋值直接失败。 - // sloppy/strict 差异未实现,与其它只读属性写一致统一抛 TypeError。 + // 冻结数组的 length 属性不可写(writable=false),赋值直接失败; + // 两模式统一抛 TypeError(length 冻结检查的 strict/sloppy 差异不在本路径范围)。 if obj.is_frozen() { return self.raise_type_error("Cannot assign to read only property 'length'"); } @@ -215,12 +226,12 @@ impl Vm { let old_logical = obj.logical_len() as usize; let old_count = obj.array_prop_count as usize; // ArraySetLength:增长(newLen > oldLen)要求对象可扩展,不可扩展时 - // 整个赋值失败且不修改(sloppy 静默失败;VM 未实现 strict 标志)。 + // 整个赋值失败且不修改(length 失败两模式均静默 no-op,不抛)。 if raw_new_len > old_logical && !obj.is_extensible() { return Ok(()); } // ArraySetLength:收缩时若 [newLen, oldLen) 内存在不可配置元素,整个收缩 - // 失败且不做任何修改(sloppy 赋值静默失败;VM 未实现 strict 标志,统一按 no-op)。 + // 失败且不做任何修改(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) { @@ -256,12 +267,20 @@ impl Vm { if let Some(meta) = obj.prop_meta_at(pos) { if meta.is_accessor { if meta.set.is_undefined() { - return self.raise_type_error("property has no setter"); + // 无 setter:严格抛错,sloppy 静默 no-op。 + if strict { + return self.raise_type_error("property has no setter"); + } + return Ok(()); } return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); } if !meta.attributes.writable() { - return self.raise_type_error("cannot assign to read-only property"); + // 只读数据属性:严格抛错,sloppy 静默 no-op。 + if strict { + return self.raise_type_error("cannot assign to read-only property"); + } + return Ok(()); } } // pos 是存储索引(get_own_property_slot 对数组已加元素区偏移)。 @@ -272,19 +291,30 @@ impl Vm { if let Some(meta) = self.inherited_property_meta(obj, prop_name_si) { if meta.is_accessor { if meta.set.is_undefined() { - return self.raise_type_error("property has no setter"); + // 继承无 setter:严格抛错,sloppy 静默 no-op。 + if strict { + return self.raise_type_error("property has no setter"); + } + return Ok(()); } return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); } if !meta.attributes.writable() { - return self.raise_type_error("cannot assign to read-only property"); + // 继承只读数据属性:严格抛错,sloppy 静默 no-op(不遮蔽)。 + if strict { + return self.raise_type_error("cannot assign to read-only property"); + } + return Ok(()); } } // 新属性(自身与原型链均无同名):须对象可扩展(OrdinarySet 的 extensible - // 检查),不可扩展时赋值失败。 + // 检查),不可扩展时赋值失败(严格抛错,sloppy 静默 no-op)。 if !obj.is_extensible() { - return self.raise_type_error("object is not extensible"); + if strict { + return self.raise_type_error("object is not extensible"); + } + return Ok(()); } self.set_or_create_prop_value(obj, prop_name_si, val); Ok(()) @@ -293,9 +323,10 @@ impl Vm { /// TypedArray 整数索引在 `receiver` ≠ TA 时的 [[Set]] 语义:越界或非对象 /// receiver 直接返回 true(不写、不 ToNumber);界内对象 receiver 走普通 set /// 把属性落到 receiver 自身。 + #[allow(clippy::too_many_arguments)] fn set_to_receiver( &mut self, ta_obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, index: usize, - use_frame_push: bool, + use_frame_push: bool, strict: bool, ) -> Result<(), String> { let Some((_, length)) = oxide_builtins::typed_array::typed_array_integer_index(self, ta_obj, prop_name_si) else { @@ -310,7 +341,7 @@ impl Vm { } let promoted = self.promote_if_needed_for_write_ptr(receiver_ptr, val); let receiver_obj = unsafe { &mut *receiver_ptr }; - self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push) + self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push, strict) } pub(crate) fn call_or_push_setter( @@ -430,8 +461,10 @@ impl Vm { ) -> Result<(), String> { ic_trace!("set_member_prop: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); + // 写方即当前执行函数(赋值语义),strict/sloppy 判定取当前上下文。 + let strict = self.current_strict(); if obj.has_prop_meta() { - self.ordinary_set(obj, prop_name_si, val, receiver)?; + self.ordinary_set(obj, prop_name_si, val, receiver, strict)?; return Ok(()); } if crate::ic_helper::ic_set_hit_own(obj, &self.bytecode, ext_pc, val) { @@ -445,9 +478,9 @@ impl Vm { } else if self.named_prop_create_needs_ordinary_set(obj, prop_name_si) { // 数组 length / 整数索引键写新属性:分流回 ordinary_set(ArraySetLength / // 元素区写),与 dispatch_ic_set_prop 对称,防快路径建影子槽破坏数组语义。 - self.ordinary_set(obj, prop_name_si, val, receiver)?; + self.ordinary_set(obj, prop_name_si, val, receiver, strict)?; } else { - self.create_named_prop_fast(obj, prop_name_si, val, receiver, ext_pc, false)?; + self.create_named_prop_fast(obj, prop_name_si, val, receiver, ext_pc, false, strict)?; } Ok(()) } @@ -459,26 +492,40 @@ impl Vm { /// - 调用方已确认属性不存在于 shape 链(lookup_position miss)。 /// - 数组 length / 数组与 TA 整数索引键不进入本路径(调用方先分流到 ordinary_set)。 /// - `use_frame_push` 与 ordinary_set* 入口一致(member 写 false / IC_SET 分发 true)。 + /// - `strict` 为写方的严格模式:原型链只读/无 setter/不可扩展时严格抛错、sloppy 静默 no-op。 + #[allow(clippy::too_many_arguments)] pub(crate) fn create_named_prop_fast( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, ext_pc: usize, - use_frame_push: bool, + use_frame_push: bool, strict: bool, ) -> Result<(), String> { // 原型链同名 accessor 须触发 setter、只读 data 须报错(与 ordinary_set 一致), // 命中时不得创建 own 属性(规范 [[Set]] shadow 语义)。 if let Some(meta) = self.inherited_property_meta(obj, prop_name_si) { if meta.is_accessor { if meta.set.is_undefined() { - return self.raise_type_error("property has no setter"); + // 继承无 setter:严格抛错,sloppy 静默 no-op。 + if strict { + return self.raise_type_error("property has no setter"); + } + return Ok(()); } return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); } if !meta.attributes.writable() { - return self.raise_type_error("cannot assign to read-only property"); + // 继承只读数据属性:严格抛错,sloppy 静默 no-op(不遮蔽)。 + if strict { + return self.raise_type_error("cannot assign to read-only property"); + } + return Ok(()); } } // 新属性创建要求对象可扩展(规范 extensible 检查在原型链 setter/只读判定之后)。 if !obj.is_extensible() { - return self.raise_type_error("object is not extensible"); + // 不可扩展:严格抛错,sloppy 静默 no-op。 + if strict { + return self.raise_type_error("object is not extensible"); + } + return Ok(()); } // 新 shape 槽位 = 追加前命名属性数(与 push_prop 的追加位置一致)。 let slot = obj.prop_vec_len() as u32; diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 446f9177..50591aab 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -100,6 +100,12 @@ macro_rules! inline_save_field { ($recv:ident, $window_regs:ident, inline_callee, opt_copy) => { $recv.inline_callee }; + ($recv:ident, $window_regs:ident, inline_strict, copy) => { + $recv.inline_strict + }; + ($recv:ident, $window_regs:ident, inline_frames_base, copy) => { + $recv.inline_frames_base + }; ($recv:ident, $window_regs:ident, inline_args_base, copy) => { $recv.inline_args_base }; @@ -183,6 +189,12 @@ macro_rules! inline_restore_field { ($recv:ident, $saved:ident, inline_callee, opt_copy) => { $recv.inline_callee = $saved.inline_callee }; + ($recv:ident, $saved:ident, inline_strict, copy) => { + $recv.inline_strict = $saved.inline_strict + }; + ($recv:ident, $saved:ident, inline_frames_base, copy) => { + $recv.inline_frames_base = $saved.inline_frames_base + }; ($recv:ident, $saved:ident, inline_args_base, copy) => { $recv.inline_args_base = $saved.inline_args_base }; @@ -228,6 +240,8 @@ macro_rules! inline_core_fields { (spill_stack, move_field), // V M (cell_stack, move_field), // V M (inline_callee, opt_copy), // V M + (inline_strict, copy), // M + (inline_frames_base, copy), // M (inline_args_base, copy), // M (inline_args_count, copy), // M (accessor_frame_target_reg, copy), // M @@ -382,6 +396,11 @@ impl Vm { self.root_reg_limit = self.active_reg_limit; self.cell_stack.push(Vec::with_capacity(sub.cells_needed as usize)); self.inline_callee = Some(callee); + // inline 无 CallFrame:目标函数严格模式单独记录,内联执行期间的写路径 + // strict/sloppy 判定据此分派(嵌套内联时外层值由 InlineSyncState 恢复)。 + self.inline_strict = sub.is_strict; + // 帧基线快照:内联期间新压的帧(CALL/accessor)越过基线,其严格性归帧栈顶。 + self.inline_frames_base = self.frames.len(); // inline 同步调用无 CallFrame:完整实参写入 spill 栈实参区,供 CREATE_ARGUMENTS 读取。 self.inline_args_base = self.spill_stack.len() as u32; self.spill_stack.extend_from_slice(args); @@ -486,6 +505,8 @@ impl Vm { self.cell_stack.push(Vec::new()); self.sub_modules = Arc::new(collect_flat_modules(module)); self.immutables_cache = (0..self.sub_modules.len()).map(|_| OnceLock::new()).collect(); + // 顶层脚本严格模式:无帧且无 inline 时写路径的 strict/sloppy 判定来源。 + self.top_level_strict = module.is_strict; self.bytecode = Arc::clone(&module.bytecode); self.activate_immutables(0, &module.constants); self.root_reg_limit = module.n_registers.max(1); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index dcc29b73..5cc049eb 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -75,6 +75,9 @@ impl Vm { inline_args_count: 0, accessor_frame_target_reg: None, inline_callee: None, + inline_strict: false, + inline_frames_base: 0, + top_level_strict: false, inline_reg_pool: None, generator_suspended: None, delegated_iterator: None, @@ -184,6 +187,9 @@ impl Vm { inline_args_count: 0, accessor_frame_target_reg: None, inline_callee: None, + inline_strict: false, + inline_frames_base: 0, + top_level_strict: false, inline_reg_pool: None, generator_suspended: None, delegated_iterator: None, diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 9750106c..5d2c05ac 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -124,8 +124,8 @@ fn object_define_property_rejects_mixed_descriptor() { } #[test] -fn object_define_property_non_writable_assignment_throws() { - let err = match eval("var o={}; Object.defineProperty(o,'x',{value:1}); o.x=2") { +fn object_define_property_non_writable_assignment_throws_in_strict() { + let err = match eval("var o={}; Object.defineProperty(o,'x',{value:1}); (function(){'use strict'; o.x=2})()") { Ok(_) => panic!("expected assignment to fail"), Err(err) => err, }; diff --git a/crates/oxide_vm/tests/member_write_ic_tests.rs b/crates/oxide_vm/tests/member_write_ic_tests.rs index 2f6f4d19..391613ba 100644 --- a/crates/oxide_vm/tests/member_write_ic_tests.rs +++ b/crates/oxide_vm/tests/member_write_ic_tests.rs @@ -90,16 +90,18 @@ fn proto_setter_called_both_write_paths() { assert_eq!(r, "2:0", "setter 被调 2 次(IC_SET 与 member 各 1),o 无 own 属性"); } -/// 原型只读 data:两条写路径都按 ordinary_set 抛 TypeError。 +/// 严格模式原型只读 data:两条写路径都按 ordinary_set 抛 TypeError。 #[test] -fn readonly_inherited_data_raises() { +fn readonly_inherited_data_raises_in_strict() { let (r, _hits, _misses) = run_once( r#"var p = {}; Object.defineProperty(p, 'x', { value: 1, writable: false }); var o = Object.create(p); var msg = ''; - try { o.x = 5; } catch (e) { msg = e.message; } - try { o.x += 1; } catch (e) { msg += '|' + e.message; } + (function() { 'use strict'; + try { o.x = 5; } catch (e) { msg = e.message; } + try { o.x += 1; } catch (e) { msg += '|' + e.message; } + })(); msg"#, ); assert_eq!( @@ -108,6 +110,21 @@ fn readonly_inherited_data_raises() { ); } +/// sloppy 原型只读 data:两条写路径都静默 no-op(值不变、不抛)。 +#[test] +fn readonly_inherited_data_silently_fails_in_sloppy() { + let (r, _hits, _misses) = run_once( + r#"var p = {}; + Object.defineProperty(p, 'x', { value: 1, writable: false }); + var o = Object.create(p); + var threw = false; + try { o.x = 5; } catch (e) { threw = true; } + try { o.x += 1; } catch (e) { threw = true; } + threw + ':' + o.x + ':' + o.hasOwnProperty('x')"#, + ); + assert_eq!(r, "false:1:false", "sloppy 只读继承属性写静默失败,值保持 1,不创建 own 属性"); +} + /// 写新属性(shape 转换):快路径建 shape + 写回,循环内同 shape 写全命中。 #[test] fn write_new_prop_then_hit() { diff --git a/crates/oxide_vm/tests/property_array_fastpath_tests.rs b/crates/oxide_vm/tests/property_array_fastpath_tests.rs index 9f1eb042..e0ddf119 100644 --- a/crates/oxide_vm/tests/property_array_fastpath_tests.rs +++ b/crates/oxide_vm/tests/property_array_fastpath_tests.rs @@ -89,16 +89,26 @@ fn accessor_getter_and_setter_still_trigger() { ); } -// 非写属性(writable:false)写入:fast path 不命中,走完整协议抛 TypeError。 +// 非写属性(writable:false)写入(严格模式):fast path 不命中,走完整协议抛 TypeError。 #[test] -fn non_writable_index_write_throws() { +fn non_writable_index_write_throws_in_strict() { let r = eval_str( - "var a=[1,2,3]; Object.defineProperty(a,0,{writable:false,value:1}); try { a[0]=9; 'no' } catch(e) { e instanceof TypeError ? 'throws' : 'wrong' }" + "var a=[1,2,3]; Object.defineProperty(a,0,{writable:false,value:1}); var r='no'; (function(){'use strict'; try { a[0]=9; } catch(e) { r = (e instanceof TypeError ? 'throws' : 'wrong'); } })(); r" ) .unwrap(); assert_eq!(r, "throws", "expected TypeError, got {r:?}"); } +// 非写属性(writable:false)写入(sloppy):静默 no-op,值不变。 +#[test] +fn non_writable_index_write_silently_fails_in_sloppy() { + let r = eval_str( + "var a=[1,2,3]; Object.defineProperty(a,0,{writable:false,value:1}); try { a[0]=9; 'no' } catch(e) { 'threw' }; a[0]" + ) + .unwrap(); + assert_eq!(r, "1", "sloppy 只读元素写应静默 no-op,值保持 1,实际 {r:?}"); +} + // 非规范数字串(前导零)不映射整数键,走字符串键慢路径(不命中 fast path)。 #[test] fn non_canonical_index_string_uses_slow_path() { diff --git a/crates/oxide_vm/tests/property_write_protection_tests.rs b/crates/oxide_vm/tests/property_write_protection_tests.rs index a595bd6d..185b80a5 100644 --- a/crates/oxide_vm/tests/property_write_protection_tests.rs +++ b/crates/oxide_vm/tests/property_write_protection_tests.rs @@ -33,17 +33,30 @@ fn assert_err_contains(result: Result, expected: &str) { } } -// ── 向不可写自身属性赋值抛 TypeError ── +// ── 严格模式向不可写自身属性赋值抛 TypeError ── #[test] -fn assign_to_non_writable_throws() { +fn assign_to_non_writable_throws_in_strict() { let result = eval_many(&[ "var obj = {}", "Object.defineProperty(obj, 'x', {value: 1, writable: false})", - "obj.x = 2", + "(function() { 'use strict'; obj.x = 2 })()", ]); assert_err_contains(result, "read-only"); } +// ── sloppy 向不可写自身属性赋值静默失败(值不变、不抛) ── +#[test] +fn assign_to_non_writable_silently_fails_in_sloppy() { + let r = eval_many(&[ + "var obj = {}", + "Object.defineProperty(obj, 'x', {value: 1, writable: false})", + "obj.x = 2", + "obj.x", + ]) + .unwrap(); + assert!(r.is_int() && r.as_int() == 1, "sloppy 只读属性写应静默 no-op,值保持 1,实际 {:?}", r); +} + // ── 向可写自身属性赋值成功 ── #[test] fn assign_to_writable_succeeds() { @@ -57,46 +70,87 @@ fn assign_to_writable_succeeds() { assert!(r.is_int() && r.as_int() == 2, "assign to writable should set value to 2, got {:?}", r); } -// ── 向原型上不可写的继承属性赋值抛错 ── +// ── 严格模式向原型上不可写的继承属性赋值抛错 ── #[test] -fn assign_to_inherited_non_writable_throws() { +fn assign_to_inherited_non_writable_throws_in_strict() { let result = eval_many(&[ "var proto = {}", "Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})", "var child = Object.create(proto)", - "child.x = 2", + "(function() { 'use strict'; child.x = 2 })()", ]); assert_err_contains(result, "read-only"); } -// ── 原型属性不可写但子对象无自有属性时,赋值会在子对象上创建自有属性 -// ponytail: spec allows shadowing when proto prop is configurable; engine doesn't yet. -// 引擎对不可写原型属性正确抛错;完整遮蔽作为后续工作。 +// ── 继承只读数据属性:sloppy 赋值静默失败,不创建自有属性(不遮蔽) ── #[test] -fn assign_non_writable_proto_configurable_prop_throws() { +fn assign_to_inherited_non_writable_silently_fails_in_sloppy() { + let (_vm, r) = eval_keep_vm( + &[ + "var proto = {}", + "Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})", + "var child = Object.create(proto)", + "child.x = 99", + "[child.x, child.hasOwnProperty('x')]", + ] + .join("; "), + ) + .unwrap(); + let arr = unsafe { &*r.as_js_object_ptr() }; + assert!( + arr.get_prop_at(0).is_int() && arr.get_prop_at(0).as_int() == 1, + "sloppy 写只读继承属性应静默 no-op,值保持 1" + ); + assert!( + arr.get_prop_at(1).is_bool() && !arr.get_prop_at(1).as_bool(), + "静默失败不得在子对象上创建自有属性" + ); +} + +// ── 严格模式:原型只读可配置属性赋值抛错(不可写数据属性不可遮蔽) ── +#[test] +fn assign_non_writable_proto_configurable_prop_throws_in_strict() { let result = eval_many(&[ "var proto = {}", "Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})", "var child = Object.create(proto)", - "child.x = 99", + "(function() { 'use strict'; child.x = 99 })()", ]); assert_err_contains(result, "read-only"); } -// ── Object.freeze 后写已有属性抛 TypeError ── +// ── 严格模式 Object.freeze 后写已有属性抛 TypeError ── #[test] -fn frozen_write_existing_prop_throws() { - let result = eval_many(&["var o = Object.freeze({a: 1})", "o.a = 2"]); +fn frozen_write_existing_prop_throws_in_strict() { + let result = eval_many(&["var o = Object.freeze({a: 1})", "(function() { 'use strict'; o.a = 2 })()"]); assert_err_contains(result, "read-only"); } -// ── Object.freeze 后写新属性抛 TypeError ── +// ── sloppy Object.freeze 后写已有属性静默失败 ── #[test] -fn frozen_write_new_prop_throws() { - let result = eval_many(&["var o = Object.freeze({a: 1})", "o.b = 2"]); +fn frozen_write_existing_prop_silently_fails_in_sloppy() { + let r = eval_many(&["var o = Object.freeze({a: 1})", "o.a = 2", "o.a"]).unwrap(); + assert!(r.is_int() && r.as_int() == 1, "sloppy 冻结对象写应静默 no-op,值保持 1,实际 {:?}", r); +} + +// ── 严格模式 Object.freeze 后写新属性抛 TypeError ── +#[test] +fn frozen_write_new_prop_throws_in_strict() { + let result = eval_many(&["var o = Object.freeze({a: 1})", "(function() { 'use strict'; o.b = 2 })()"]); assert_err_contains(result, "not extensible"); } +// ── sloppy Object.freeze 后写新属性静默失败(属性不创建) ── +#[test] +fn frozen_write_new_prop_silently_fails_in_sloppy() { + let r = eval_many(&["var o = Object.freeze({a: 1})", "o.b = 2", "o.b === undefined"]).unwrap(); + assert!( + r.is_bool() && r.as_bool(), + "sloppy 冻结对象写新属性应静默 no-op,属性不创建,实际 {:?}", + r + ); +} + // ── Object.freeze 后 defineProperty 抛 TypeError ── #[test] fn frozen_define_property_throws() { @@ -104,13 +158,14 @@ fn frozen_define_property_throws() { assert_err_contains(result, "not extensible"); } -// ── Object.seal 后写新属性抛错、删属性返回 false、改值生效 ── +// ── 严格模式 Object.seal 后写新属性抛错、删属性返回 false、改值生效 ── #[test] fn sealed_semantics() { let (_vm, r) = eval_keep_vm( &[ "var o = Object.seal({a: 1})", - "var new_throws = false; try { o.b = 2 } catch (e) { new_throws = true }", + "var new_throws = false", + "(function() { 'use strict'; try { o.b = 2 } catch (e) { new_throws = true } })()", "var del = delete o.a", "o.a = 5", "[new_throws, del, o.a, o.hasOwnProperty('a')]", @@ -121,30 +176,30 @@ fn sealed_semantics() { let arr = unsafe { &*r.as_js_object_ptr() }; assert!( arr.get_prop_at(0).is_bool() && arr.get_prop_at(0).as_bool(), - "new prop write should throw" + "strict new prop write should throw" ); assert!(arr.get_prop_at(1).is_bool() && !arr.get_prop_at(1).as_bool(), "delete should be false"); assert!(arr.get_prop_at(2).is_int() && arr.get_prop_at(2).as_int() == 5, "value write should work"); assert!(arr.get_prop_at(3).is_bool() && arr.get_prop_at(3).as_bool(), "prop should remain own"); } -// ── Object.preventExtensions 后新属性不生效(sloppy 抛错),已有属性可写 ── +// ── Object.preventExtensions 后新属性写静默失效(sloppy),已有属性可写 ── #[test] fn prevent_extensions_semantics() { let (_vm, r) = eval_keep_vm( &[ "var o = Object.preventExtensions({a: 1})", - "var new_throws = false; try { o.b = 3 } catch (e) { new_throws = true }", + "var new_threw = false; try { o.b = 3 } catch (e) { new_threw = true }", "o.a = 2", - "[new_throws, o.a, o.b === undefined]", + "[new_threw, o.a, o.b === undefined]", ] .join("; "), ) .unwrap(); let arr = unsafe { &*r.as_js_object_ptr() }; assert!( - arr.get_prop_at(0).is_bool() && arr.get_prop_at(0).as_bool(), - "new prop write should throw" + arr.get_prop_at(0).is_bool() && !arr.get_prop_at(0).as_bool(), + "sloppy new prop write should silently fail" ); assert!( arr.get_prop_at(1).is_int() && arr.get_prop_at(1).as_int() == 2, @@ -153,15 +208,15 @@ fn prevent_extensions_semantics() { assert!(arr.get_prop_at(2).is_bool() && arr.get_prop_at(2).as_bool(), "new prop should be absent"); } -// ── Object.freeze 数组:元素写与 length 收缩均被拦截 ── +// ── 严格模式 Object.freeze 数组:元素写与 length 收缩均抛错 ── #[test] -fn frozen_array_write_and_length_throws() { +fn frozen_array_write_and_length_throws_in_strict() { let (_vm, r) = eval_keep_vm( &[ "var a = Object.freeze([1, 2])", "var e1 = false, e2 = false", - "try { a[0] = 9 } catch (e) { e1 = true }", - "try { a.length = 0 } catch (e) { e2 = true }", + "(function() { 'use strict'; try { a[0] = 9 } catch (e) { e1 = true } })()", + "(function() { 'use strict'; try { a.length = 0 } catch (e) { e2 = true } })()", "[e1, e2, a[0], a.length, Object.getOwnPropertyDescriptor(a, '0').writable]", ] .join("; "), @@ -178,6 +233,27 @@ fn frozen_array_write_and_length_throws() { ); } +// ── sloppy Object.freeze 数组:元素写静默失败(length 冻结检查仍抛错) ── +#[test] +fn frozen_array_element_write_silently_fails_in_sloppy() { + let (_vm, r) = eval_keep_vm( + &[ + "var a = Object.freeze([1, 2])", + "var e1 = false", + "try { a[0] = 9 } catch (e) { e1 = true }", + "[e1, a[0]]", + ] + .join("; "), + ) + .unwrap(); + let arr = unsafe { &*r.as_js_object_ptr() }; + assert!( + arr.get_prop_at(0).is_bool() && !arr.get_prop_at(0).as_bool(), + "sloppy 冻结数组元素写应静默 no-op" + ); + assert!(arr.get_prop_at(1).is_int() && arr.get_prop_at(1).as_int() == 1, "element value unchanged"); +} + // ── isFrozen/isSealed:手动 defineProperty 全部冻结(未调 freeze)也应判 true ── #[test] fn is_frozen_manual_define_props() { @@ -212,19 +288,40 @@ fn reflect_ops_on_frozen_return_false() { } } -// ── freeze/seal 后 IC 直写路径失效:循环写仍被拦截 ── +// ── 严格模式 freeze 后 IC 直写路径失效:循环写仍被拦截 ── #[test] -fn frozen_ic_path_still_blocks_write() { +fn frozen_ic_path_still_blocks_write_in_strict() { let r = eval_many(&[ "var o = Object.freeze({a: 1})", "var threw = false", - "for (var i = 0; i < 3; i++) { try { o.a = i } catch (e) { threw = true } }", + "(function() { 'use strict'; for (var i = 0; i < 3; i++) { try { o.a = i } catch (e) { threw = true } } })()", "threw", ]) .unwrap(); assert!(r.is_bool() && r.as_bool(), "IC path should still throw on frozen write"); } +// ── sloppy freeze 后循环写静默失效(值不变、不抛) ── +#[test] +fn frozen_ic_path_silently_fails_in_sloppy() { + let (_vm, r) = eval_keep_vm( + &[ + "var o = Object.freeze({a: 1})", + "var threw = false", + "for (var i = 0; i < 3; i++) { try { o.a = i } catch (e) { threw = true } }", + "[threw, o.a]", + ] + .join("; "), + ) + .unwrap(); + let arr = unsafe { &*r.as_js_object_ptr() }; + assert!( + arr.get_prop_at(0).is_bool() && !arr.get_prop_at(0).as_bool(), + "sloppy frozen write should silently fail" + ); + assert!(arr.get_prop_at(1).is_int() && arr.get_prop_at(1).as_int() == 1, "value unchanged"); +} + // ── sealed 对象 IC 路径:已有属性写正常 ── #[test] fn sealed_ic_path_allows_existing_write() { diff --git a/crates/oxide_vm/tests/sloppy_assignment_silent_tests.rs b/crates/oxide_vm/tests/sloppy_assignment_silent_tests.rs new file mode 100644 index 00000000..0326f1a8 --- /dev/null +++ b/crates/oxide_vm/tests/sloppy_assignment_silent_tests.rs @@ -0,0 +1,196 @@ +//! sloppy 模式赋值静默失败语义测试:ordinary_set 失败分支(自身/继承只读数据、 +//! 无 setter 访问器、不可扩展新属性)在 sloppy 静默 no-op,严格模式抛 TypeError。 +//! 覆盖脚本顶层(`top_level_strict`)、函数级(帧 `strict`)与内联内置回调 +//! (forEach 等,`inline_strict` + 帧基线)三种执行上下文。 + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result { + 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(); + vm.run(&module) +} + +fn eval_many(lines: &[&str]) -> Result { + eval(&lines.join("; ")) +} + +fn assert_err_contains(result: Result, expected: &str) { + match result { + Ok(_) => panic!("expected error containing '{}', got Ok", expected), + Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e), + } +} + +/// 求值并把结果按字符串提取(`JsValue` Display 不携带字符串内容)。 +fn eval_str(source: &str) -> String { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&module).expect("run"); + vm.lookup_str(result).unwrap_or_else(|| format!("{result}")) +} + +// ── sloppy 脚本顶层:无 setter 访问器赋值静默 no-op(值不变、不抛) ── +#[test] +fn sloppy_script_top_level_accessor_no_setter_silently_fails() { + let r = eval_many(&["var o = { get h() { return 1 } }", "o.h = 2", "o.h"]).unwrap(); + assert!( + r.is_int() && r.as_int() == 1, + "sloppy 无 setter 访问器写应静默 no-op,值保持 1,实际 {:?}", + r + ); +} + +// ── 严格脚本(顶层指令):无 setter 访问器赋值抛 TypeError ── +#[test] +fn strict_script_top_level_accessor_no_setter_throws() { + let result = eval("'use strict'; var o = { get h() { return 1 } }; o.h = 2"); + assert_err_contains(result, "no setter"); +} + +// ── sloppy 脚本顶层:只读自身属性赋值静默失败 ── +#[test] +fn sloppy_script_top_level_readonly_silently_fails() { + let r = eval("var o = {}; Object.defineProperty(o, 'x', {value: 1, writable: false}); o.x = 2; o.x").unwrap(); + assert!(r.is_int() && r.as_int() == 1, "sloppy 只读属性写应静默 no-op,值保持 1,实际 {:?}", r); +} + +// ── 严格脚本(顶层指令):只读自身属性赋值抛 TypeError ── +#[test] +fn strict_script_top_level_readonly_throws() { + let result = eval("'use strict'; var o = {}; Object.defineProperty(o, 'x', {value: 1, writable: false}); o.x = 2"); + assert_err_contains(result, "read-only"); +} + +// ── sloppy 函数级(非严格函数):不可扩展对象写新属性静默失败 ── +#[test] +fn sloppy_function_write_new_prop_on_non_extensible_silently_fails() { + let r = eval_many(&[ + "var o = Object.preventExtensions({})", + "function w() { o.n = 3 }", + "w()", + "o.n === undefined", + ]) + .unwrap(); + assert!( + r.is_bool() && r.as_bool(), + "sloppy 函数写不可扩展对象新属性应静默 no-op,属性不创建,实际 {:?}", + r + ); +} + +// ── 严格函数:不可扩展对象写新属性抛 TypeError ── +#[test] +fn strict_function_write_new_prop_on_non_extensible_throws() { + let result = eval_many(&["var o = Object.preventExtensions({})", "(function() { 'use strict'; o.n = 3 })()"]); + assert_err_contains(result, "not extensible"); +} + +// ── 继承只读数据属性:sloppy 赋值静默失败且不创建自有属性 ── +#[test] +fn sloppy_inherited_readonly_silently_fails_without_shadow() { + let s = eval_str( + "var p = {}; Object.defineProperty(p, 'x', {value: 1, writable: false, configurable: true}); \ + var c = Object.create(p); c.x = 9; c.x + ':' + c.hasOwnProperty('x')", + ); + assert_eq!(s, "1:false", "sloppy 只读继承属性写应静默 no-op,值保持 1 且不遮蔽"); +} + +// ── 内联回调(forEach):sloppy 顶层 + sloppy 回调,只读属性写静默失败 ── +#[test] +fn sloppy_inline_callback_write_to_readonly_silently_fails() { + let r = eval_many(&[ + "var o = {}", + "Object.defineProperty(o, 'p', {value: 0, writable: false, configurable: true})", + "[1].forEach(function() { o.p = 1 })", + "o.p", + ]) + .unwrap(); + assert!(r.is_int() && r.as_int() == 0, "sloppy 内联回调内只读属性写应静默 no-op,实际 {:?}", r); +} + +// ── 内联回调:严格函数上下文(回调继承严格),只读属性写抛 TypeError ── +#[test] +fn strict_inline_callback_write_to_readonly_throws() { + let r = eval_many(&[ + "var o = {}", + "Object.defineProperty(o, 'p', {value: 0, writable: false, configurable: true})", + "var threw = false", + "(function() { 'use strict'; [1].forEach(function() { try { o.p = 1 } catch (e) { threw = true } }) })()", + "threw", + ]) + .unwrap(); + assert!(r.is_bool() && r.as_bool(), "严格内联回调内只读属性写应抛 TypeError,实际 {:?}", r); +} + +// ── 嵌套内联:sloppy 回调内调用严格动态函数,写只读属性抛 TypeError +// (严格性随内联快照逐层保存/恢复,内层取内层目标) ── +#[test] +fn nested_inline_strict_target_write_throws() { + let r = eval_many(&[ + "var o = {}", + "Object.defineProperty(o, 'p', {value: 0, writable: false, configurable: true})", + "var strictWrite = new Function('o', \"'use strict'; o.p = 1\")", + "var threw = false", + "[1].forEach(function() { try { strictWrite(o) } catch (e) { threw = true } })", + "threw", + ]) + .unwrap(); + assert!( + r.is_bool() && r.as_bool(), + "嵌套内联中严格目标函数的只读属性写应抛 TypeError,实际 {:?}", + r + ); +} + +// ── 嵌套内联反向:严格回调内调用 sloppy 动态函数,写只读属性静默失败 ── +#[test] +fn nested_inline_sloppy_target_write_silently_fails() { + let r = eval_many(&[ + "var o = {}", + "Object.defineProperty(o, 'p', {value: 0, writable: false, configurable: true})", + "var sloppyWrite = new Function('o', 'o.p = 1')", + "(function() { 'use strict'; [1].forEach(function() { sloppyWrite(o) }) })()", + "o.p", + ]) + .unwrap(); + assert!( + r.is_int() && r.as_int() == 0, + "嵌套内联中 sloppy 目标函数的只读属性写应静默 no-op,实际 {:?}", + r + ); +} + +// ── 链式赋值中间对象只读(冻结):sloppy 对中间对象写新属性静默失败 ── +#[test] +fn sloppy_chained_write_through_readonly_middle_silently_fails() { + let s = eval_str( + "var holder = Object.freeze({}); var o = { b: holder }; o.b.c = 1; \ + Object.keys(o.b).length + ':' + (o.b.c === undefined)", + ); + assert_eq!(s, "0:true", "sloppy 链式中间冻结对象写新属性应静默 no-op,c 不创建"); +} + +// ── 链式赋值中间对象只读(冻结):严格模式抛 TypeError ── +#[test] +fn strict_chained_write_through_readonly_middle_throws() { + let result = eval_many(&[ + "var holder = Object.freeze({})", + "var o = { b: holder }", + "(function() { 'use strict'; o.b.c = 1 })()", + ]); + assert_err_contains(result, "not extensible"); +} + +// ── 顶层脚本 sloppy 赋值只读全局属性(undefined)不抛错 ── +#[test] +fn sloppy_top_level_assign_to_readonly_global_no_throw() { + let r = eval_many(&["undefined = 1", "typeof undefined"]).unwrap(); + assert!(r.is_string(), "sloppy 顶层对只读全局属性赋值不得抛错,实际 {:?}", r); +}