diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 51db67a1..12b0004f 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -1256,11 +1256,12 @@ pub fn object_get_prototype_of(vm: &mut H, args: &[u8]) -> NativeResu /// - `obj` 为 null/undefined 抛 TypeError(RequireObjectCoercible);原始值原样返回, /// 不装箱。 /// - `proto` 既非对象也非 null 时抛 TypeError。 -/// - 目标不可设置(原型环)时抛 TypeError;普通对象恒可设置,与 Reflect 版 -/// 按布尔返回的语义不同。 +/// - 目标不可设置(原型环,或不可扩展且新旧原型不同)时抛 TypeError;与 Reflect +/// 版按布尔返回的语义不同。 /// /// # 副作用 -/// - 目标为对象时改写其 proto 槽并递增 generation。 +/// - 目标为对象且设置成功时改写其 proto 槽并递增 generation;新旧原型相同 +/// (SameValue)时按规范直接成功。 pub fn object_set_prototype_of(vm: &mut H, args: &[u8]) -> NativeResult { let target_val = args.get(1).map(|reg| vm.reg(*reg)).unwrap_or_else(JsValue::undefined); if target_val.is_null() || target_val.is_undefined() { @@ -1280,7 +1281,16 @@ pub fn object_set_prototype_of(vm: &mut H, args: &[u8]) -> NativeResu return NativeResult::Ok(target_val); } let target_ptr = target_val.as_js_object_ptr(); - if unsafe { &mut *target_ptr }.set_proto(proto).is_err() { + let target = unsafe { &mut *target_ptr }; + // OrdinarySetPrototypeOf 步 2-4:新旧原型 SameValue 时直接成功,可扩展标志 + // 不参与判断;不可扩展且新旧不同才失败。set_proto 是裸写槽操作,不含该判定。 + if !target.is_extensible() && target.proto() != proto { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Object.setPrototypeOf: cannot set prototype of non-extensible object", + )); + } + if target.set_proto(proto).is_err() { return NativeResult::Err(crate::error::create_type_error(vm, "Object.setPrototypeOf: cannot set prototype")); } NativeResult::Ok(target_val) diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index 608b27f6..e370a7fa 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -226,6 +226,7 @@ pub fn reflect_set(vm: &mut H, args: &[u8]) -> NativeResult { } /// `Reflect.setPrototypeOf(target, proto)`:设置 prototype(对象或 null)。 +/// 不可扩展且新旧原型不同时返回 false;新旧相同返回 true。 pub fn reflect_set_prototype_of(vm: &mut H, args: &[u8]) -> NativeResult { let target_val = arg(vm, args, 1); let Some(target_ptr) = object_ptr(target_val) else { @@ -235,7 +236,12 @@ pub fn reflect_set_prototype_of(vm: &mut H, args: &[u8]) -> NativeRes if !proto.is_object() && !proto.is_null() { return type_error(vm, "Reflect.setPrototypeOf prototype must be an object or null"); } - NativeResult::Ok(JsValue::bool(unsafe { &mut *target_ptr }.set_proto(proto).is_ok())) + let target = unsafe { &mut *target_ptr }; + // OrdinarySetPrototypeOf 步 2-4:新旧相同先于可扩展判定,直接成功。 + if !target.is_extensible() && target.proto() != proto { + return NativeResult::Ok(JsValue::bool(false)); + } + NativeResult::Ok(JsValue::bool(target.set_proto(proto).is_ok())) } fn arg(vm: &H, args: &[u8], idx: usize) -> JsValue { diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index dc4c8c3f..32b08668 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -250,7 +250,10 @@ impl Emitter { self.emit_throw_error("ReferenceError", "Cannot delete a super reference", ctx) } Expression::ComputedMemberExpression(member) if matches!(&member.object, Expression::Super(_)) => { - // super 属性引用不可删除:键表达式与基均不得先行求值。 + // super 属性引用不可删除:立即抛 ReferenceError,不执行 ToPropertyKey, + // 也不求值基与 this。代价:this 已初始化时键表达式副作用被跳过—— + // 未初始化须先抛与副作用须先执行在缺少 GetThisBinding 运行期检查前 + // 无法兼得。 self.emit_throw_error("ReferenceError", "Cannot delete a super reference", ctx) } Expression::StaticMemberExpression(member) => { diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 8c9416d7..ec78a4ac 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -773,6 +773,12 @@ impl JsObject { /// /// 仅接受 `null` 或对象;沿新原型链检查是否构成环,成环返回 /// `Err("cyclic __proto__ value")`。 + /// + /// # 注意事项 + /// 本方法是裸写槽操作:既不检查 `[[Extensible]]`,也不比较新旧原型是否 + /// SameValue。实现 `[[SetPrototypeOf]]` 的调用方须先自行判定「新旧相同直接 + /// 成功」「不可扩展且新旧不同则失败」,再调用本方法;原型接线、世界重建等 + /// 内部路径不受可扩展标志约束,直接调用即可。 pub fn set_proto(&mut self, proto: JsValue) -> Result<(), &'static str> { if proto.is_null() { self.proto = proto; diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index a3e86725..1b5cdcc8 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -330,6 +330,39 @@ fn object_set_prototype_of_sets_prototype_and_validates() { assert!(err.contains("TypeError"), "unexpected error: {err}"); } +// Object.setPrototypeOf 负向分路:不可扩展目标、原型环、原始值目标。 +#[test] +fn object_set_prototype_of_negative_paths() { + // 不可扩展且新旧原型不同抛 TypeError。 + let err = match eval("var o = {}; Object.preventExtensions(o); Object.setPrototypeOf(o, null);") { + Ok(_) => panic!("non-extensible target with a different prototype should fail"), + Err(err) => err, + }; + assert!(err.contains("TypeError"), "unexpected error: {err}"); + + // 不可扩展但新旧原型相同仍成功,符合规范「SameValue 先于可扩展判定」。 + let (_vm, result) = eval( + "var o = {}; var p = Object.getPrototypeOf(o); Object.preventExtensions(o); \ + Object.setPrototypeOf(o, p) === o", + ) + .unwrap(); + assert!( + result.is_bool() && result.as_bool(), + "same prototype on non-extensible target should succeed" + ); + + // 原型环抛 TypeError。 + let err = match eval("var a = {}; var b = {}; Object.setPrototypeOf(a, b); Object.setPrototypeOf(b, a);") { + Ok(_) => panic!("cyclic prototype should fail"), + Err(err) => err, + }; + assert!(err.contains("TypeError"), "unexpected error: {err}"); + + // 原始值目标原样返回,不抛错。 + let (_vm, result) = eval("Object.setPrototypeOf(1, null)").unwrap(); + assert!(result.is_int() && result.as_int() == 1, "primitive target should be returned unchanged"); +} + #[test] fn entries_values_with_interleaved_symbol_key() { let (_vm, result) = eval( diff --git a/crates/oxide_vm/tests/builtins_reflect_tests.rs b/crates/oxide_vm/tests/builtins_reflect_tests.rs index 4422e272..37393e68 100644 --- a/crates/oxide_vm/tests/builtins_reflect_tests.rs +++ b/crates/oxide_vm/tests/builtins_reflect_tests.rs @@ -82,6 +82,22 @@ fn reflect_prototype_and_extensible_methods() { assert!(result.as_bool()); } +// 不可扩展目标:新旧不同返回 false,新旧相同返回 true,且原值不被改写。 +#[test] +fn reflect_set_prototype_of_non_extensible_returns_false() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var a = {}; Object.preventExtensions(a); \ + var diff = Reflect.setPrototypeOf(a, null); \ + var kept = Object.getPrototypeOf(a) === Object.prototype; \ + var same = Reflect.setPrototypeOf(a, Object.prototype); \ + diff === false && kept && same === true", + ) + .unwrap(); + assert!(result.as_bool()); +} + #[test] fn reflect_apply_calls_function() { let mut vm = Vm::new();