From d7143e24bac60375c0c35f53bee2622c584a6a9e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 14 Jun 2026 18:10:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20Reflect=20?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/bindings/bind_reflect.rs | 41 +++ crates/oxide_vm/src/bindings/mod.rs | 2 + crates/oxide_vm/src/builtins/mod.rs | 1 + crates/oxide_vm/src/builtins/reflect.rs | 273 ++++++++++++++++++ .../oxide_vm/tests/builtins_reflect_tests.rs | 113 ++++++++ 5 files changed, 430 insertions(+) create mode 100644 crates/oxide_vm/src/bindings/bind_reflect.rs create mode 100644 crates/oxide_vm/src/builtins/reflect.rs create mode 100644 crates/oxide_vm/tests/builtins_reflect_tests.rs diff --git a/crates/oxide_vm/src/bindings/bind_reflect.rs b/crates/oxide_vm/src/bindings/bind_reflect.rs new file mode 100644 index 00000000..bc8bb5c0 --- /dev/null +++ b/crates/oxide_vm/src/bindings/bind_reflect.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use crate::bindings::{apply_binding_table, bind_global_value}; +use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +pub fn bind_reflect(core: &Arc, session: &KernelSession, global: &mut JsObject) { + let mut reflect = Box::new(JsObject::new_empty( + EMPTY_SHAPE_ID, + JsValue::from_js_object(session.builtin_world().object_proto.as_ptr() as *mut JsObject), + )); + + apply_binding_table( + session.builtin_world(), + &mut reflect, + core, + &[ + ("apply", crate::builtins::reflect::reflect_apply as *const (), 3), + ("construct", crate::builtins::reflect::reflect_construct as *const (), 2), + ("defineProperty", crate::builtins::reflect::reflect_define_property as *const (), 3), + ("deleteProperty", crate::builtins::reflect::reflect_delete_property as *const (), 2), + ("get", crate::builtins::reflect::reflect_get as *const (), 2), + ( + "getOwnPropertyDescriptor", + crate::builtins::reflect::reflect_get_own_property_descriptor as *const (), + 2, + ), + ("getPrototypeOf", crate::builtins::reflect::reflect_get_prototype_of as *const (), 1), + ("has", crate::builtins::reflect::reflect_has as *const (), 2), + ("isExtensible", crate::builtins::reflect::reflect_is_extensible as *const (), 1), + ("ownKeys", crate::builtins::reflect::reflect_own_keys as *const (), 1), + ("preventExtensions", crate::builtins::reflect::reflect_prevent_extensions as *const (), 1), + ("set", crate::builtins::reflect::reflect_set as *const (), 3), + ("setPrototypeOf", crate::builtins::reflect::reflect_set_prototype_of as *const (), 2), + ], + ); + + bind_global_value(core, global, "Reflect", JsValue::from_js_object(Box::into_raw(reflect))); +} diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index eaddb37a..c9ca90f5 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -10,6 +10,7 @@ pub mod bind_map; pub mod bind_math; pub mod bind_number; pub mod bind_object; +pub mod bind_reflect; pub mod bind_regexp; pub mod bind_set; pub mod bind_string; @@ -99,6 +100,7 @@ pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) bind_regexp::bind_regexp(core, session, global); bind_symbol::bind_symbol(core, session, global); bind_iterator::bind_iterator(core, session, global); + bind_reflect::bind_reflect(core, session, global); bind_global::bind_global(core, session, global); bind_stubs::bind_stubs(core, session, global); diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index 40af9abd..1c62c8af 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -10,6 +10,7 @@ pub mod map; pub mod math; pub mod number; pub mod object; +pub mod reflect; pub mod regexp; pub mod set; pub mod string; diff --git a/crates/oxide_vm/src/builtins/reflect.rs b/crates/oxide_vm/src/builtins/reflect.rs new file mode 100644 index 00000000..e5d5190f --- /dev/null +++ b/crates/oxide_vm/src/builtins/reflect.rs @@ -0,0 +1,273 @@ +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; +use oxide_types::value::JsValue; + +use crate::builtins::object::walk_own_keys; +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; + +pub fn reflect_apply(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target = arg(vm, args, 1); + let this_arg = arg(vm, args, 2); + let arg_list = arg(vm, args, 3); + if !is_callable(target) { + return type_error(vm, "Reflect.apply target is not callable"); + } + let Some(call_args) = array_like_elements(arg_list) else { + return type_error(vm, "Reflect.apply argumentsList must be an array-like object"); + }; + match vm.call_function_sync(target, this_arg, &call_args) { + Ok(value) => NativeResult::Ok(value), + Err(err) => type_error(vm, &err), + } +} + +pub fn reflect_construct(vm: &mut Vm, _args: &[u8]) -> NativeResult { + type_error(vm, "Reflect.construct not yet supported") +} + +pub fn reflect_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.defineProperty target is not an object"); + }; + let desc_val = arg(vm, args, 3); + let Some(desc_ptr) = object_ptr(desc_val) else { + return type_error(vm, "Reflect.defineProperty descriptor is not an object"); + }; + + let key_si = vm.property_key_si(arg(vm, args, 2)); + let desc = unsafe { &*desc_ptr }; + let value_si = vm.kernel_core().string_forge().intern("value").0; + let get_si = vm.kernel_core().string_forge().intern("get").0; + let set_si = vm.kernel_core().string_forge().intern("set").0; + let writable_si = vm.kernel_core().string_forge().intern("writable").0; + let enumerable_si = vm.kernel_core().string_forge().intern("enumerable").0; + let configurable_si = vm.kernel_core().string_forge().intern("configurable").0; + + let value_field = own_field(vm, desc, value_si); + let get_field = own_field(vm, desc, get_si); + let set_field = own_field(vm, desc, set_si); + let writable_field = own_field(vm, desc, writable_si); + let enumerable = own_field(vm, desc, enumerable_si) + .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) + .unwrap_or(false); + let configurable = own_field(vm, desc, configurable_si) + .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) + .unwrap_or(false); + + let has_data = value_field.is_some() || writable_field.is_some(); + let has_accessor = get_field.is_some() || set_field.is_some(); + if has_data && has_accessor { + return NativeResult::Ok(JsValue::bool(false)); + } + + let target = unsafe { &mut *target_ptr }; + let result = if has_accessor { + let get = get_field.unwrap_or(JsValue::undefined()); + let set = set_field.unwrap_or(JsValue::undefined()); + if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) { + return NativeResult::Ok(JsValue::bool(false)); + } + vm.define_accessor_property(target, key_si, get, set, PropAttributes::new(false, enumerable, configurable)) + } else { + let value = value_field.unwrap_or(JsValue::undefined()); + let writable = writable_field + .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) + .unwrap_or(false); + vm.define_data_property(target, key_si, value, PropAttributes::new(writable, enumerable, configurable)) + }; + NativeResult::Ok(JsValue::bool(result.is_ok())) +} + +pub fn reflect_delete_property(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.deleteProperty target is not an object"); + }; + let key_si = vm.property_key_si(arg(vm, args, 2)); + let target = unsafe { &mut *target_ptr }; + NativeResult::Ok(JsValue::bool(delete_own_property(vm, target, key_si))) +} + +pub fn reflect_get(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.get target is not an object"); + }; + let key_si = vm.property_key_si(arg(vm, args, 2)); + let receiver = if args.len() > 3 { vm.reg(args[3]) } else { target_val }; + match vm.ordinary_get(unsafe { &*target_ptr }, key_si, receiver) { + Ok(value) => NativeResult::Ok(value), + Err(err) => type_error(vm, &err), + } +} + +pub fn reflect_get_own_property_descriptor(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(_) = object_ptr(target_val) else { + return type_error(vm, "Reflect.getOwnPropertyDescriptor target is not an object"); + }; + crate::builtins::object::object_get_own_property_descriptor(vm, args) +} + +pub fn reflect_get_prototype_of(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.getPrototypeOf target is not an object"); + }; + NativeResult::Ok(unsafe { &*target_ptr }.proto()) +} + +pub fn reflect_has(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.has target is not an object"); + }; + let key_si = vm.property_key_si(arg(vm, args, 2)); + NativeResult::Ok(JsValue::bool(vm.resolve_property(unsafe { &*target_ptr }, key_si).is_some())) +} + +pub fn reflect_is_extensible(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.isExtensible target is not an object"); + }; + NativeResult::Ok(JsValue::bool(unsafe { &*target_ptr }.is_extensible())) +} + +pub fn reflect_own_keys(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.ownKeys target is not an object"); + }; + let target = unsafe { &*target_ptr }; + let key_names: Vec = walk_own_keys(vm, target) + .into_iter() + .map(|(si, _)| vm.kernel_core().string_forge().lookup(si).unwrap_or_default()) + .collect(); + NativeResult::Ok(make_string_array(vm, &key_names)) +} + +pub fn reflect_prevent_extensions(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.preventExtensions target is not an object"); + }; + unsafe { &mut *target_ptr }.set_extensible(false); + NativeResult::Ok(JsValue::bool(true)) +} + +pub fn reflect_set(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.set target is not an object"); + }; + let key_si = vm.property_key_si(arg(vm, args, 2)); + let value = arg(vm, args, 3); + let receiver = if args.len() > 4 { vm.reg(args[4]) } else { target_val }; + let target = unsafe { &mut *target_ptr }; + 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())) +} + +pub fn reflect_set_prototype_of(vm: &mut Vm, args: &[u8]) -> NativeResult { + let target_val = arg(vm, args, 1); + let Some(target_ptr) = object_ptr(target_val) else { + return type_error(vm, "Reflect.setPrototypeOf target is not an object"); + }; + let proto = arg(vm, args, 2); + 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())) +} + +fn arg(vm: &Vm, args: &[u8], idx: usize) -> JsValue { + args.get(idx).map(|reg| vm.reg(*reg)).unwrap_or_else(JsValue::undefined) +} + +fn object_ptr(value: JsValue) -> Option<*mut JsObject> { + if !value.is_object() { + return None; + } + let ptr = value.as_js_object_ptr(); + (!ptr.is_null()).then_some(ptr) +} + +fn is_callable(value: JsValue) -> bool { + object_ptr(value).is_some_and(|ptr| unsafe { &*ptr }.is_function()) +} + +fn type_error(vm: &mut Vm, message: &str) -> NativeResult { + NativeResult::Err(crate::builtins::error::create_type_error(vm, message)) +} + +fn array_like_elements(value: JsValue) -> Option> { + let ptr = object_ptr(value)?; + let obj = unsafe { &*ptr }; + Some((0..obj.prop_count() as usize).map(|idx| obj.get_prop_at(idx)).collect()) +} + +fn own_field(vm: &Vm, obj: &JsObject, prop_si: u32) -> Option { + vm.kernel_core() + .shape_forge() + .lookup_position(obj.shape_id(), prop_si) + .and_then(|pos| (obj.prop_vec_len() > pos as usize).then(|| obj.get_prop_at(pos))) +} + +fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue { + let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; + let arr = vm.epoch().alloc(JsObject::new_array( + EMPTY_SHAPE_ID, + JsValue::from_js_object(proto), + parts.len(), + vm.epoch().bump(), + )); + for (idx, part) in parts.iter().enumerate() { + let value = vm.intern(part); + unsafe { &mut *arr }.set_prop_at(idx, value); + } + unsafe { &mut *arr }.set_prop_count(parts.len()); + JsValue::from_js_object(arr) +} + +fn delete_own_property(vm: &mut Vm, obj: &mut JsObject, key_si: u32) -> bool { + let keys = walk_own_keys(vm, obj); + let Some((_, delete_pos)) = keys.iter().find(|(si, _)| *si == key_si).copied() else { + return true; + }; + if obj + .prop_meta_at(delete_pos) + .map(|meta| !meta.attributes.configurable()) + .unwrap_or(false) + { + return false; + } + + let retained: Vec<(u32, JsValue, Option)> = keys + .into_iter() + .filter(|(_, pos)| *pos != delete_pos) + .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) + .collect(); + + obj.set_shape_id(EMPTY_SHAPE_ID); + obj.set_prop_count(0usize); + for (si, value, meta) in retained { + let shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(shape); + let pos = obj.push_prop(value); + if let Some(meta) = meta { + if meta.is_accessor { + obj.set_accessor_meta(pos, meta.get, meta.set, meta.attributes); + } else { + obj.set_data_meta(pos, meta.attributes); + } + } + } + obj.bump_generation(); + true +} diff --git a/crates/oxide_vm/tests/builtins_reflect_tests.rs b/crates/oxide_vm/tests/builtins_reflect_tests.rs new file mode 100644 index 00000000..3f1fceaa --- /dev/null +++ b/crates/oxide_vm/tests/builtins_reflect_tests.rs @@ -0,0 +1,113 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, 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))?; + vm.run(&module) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.kernel_core() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() +} + +#[test] +fn reflect_global_and_methods_exist() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "typeof Reflect === 'object' && typeof Reflect.get === 'function' && typeof Reflect.construct === 'function'", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn reflect_get_set_has_delete_property() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var obj = {a: 1, b: 2}; \ + var getOk = Reflect.get(obj, 'a') === 1; \ + var setOk = Reflect.set(obj, 'c', 3) === true && obj.c === 3; \ + var hasOk = Reflect.has(obj, 'a') === true && Reflect.has(obj, 'z') === false; \ + var delOk = Reflect.deleteProperty(obj, 'a') === true && Reflect.has(obj, 'a') === false; \ + getOk && setOk && hasOk && delOk", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn reflect_own_keys_returns_own_property_names() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var obj = {a: 1, b: 2}; Reflect.set(obj, 'c', 3); Reflect.ownKeys(obj).join(',')", + ) + .unwrap(); + assert_eq!(to_str(&vm, result), "a,b,c"); +} + +#[test] +fn reflect_get_own_property_descriptor_returns_descriptor() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var obj = {}; Reflect.defineProperty(obj, 'x', { value: 9, writable: true, enumerable: true, configurable: true }); \ + var d = Reflect.getOwnPropertyDescriptor(obj, 'x'); d.value === 9 && d.writable === true", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn reflect_prototype_and_extensible_methods() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var proto = {p: 1}; var obj = {}; \ + var setProto = Reflect.setPrototypeOf(obj, proto); \ + var getProto = Reflect.getPrototypeOf(obj) === proto; \ + var ext1 = Reflect.isExtensible(obj); \ + var prevent = Reflect.preventExtensions(obj); \ + var ext2 = Reflect.isExtensible(obj); \ + setProto && getProto && ext1 === true && prevent === true && ext2 === false", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn reflect_apply_calls_function() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "function add(a, b) { return this.base + a + b; } var receiver = {base: 10}; var args = [1, 2]; Reflect.apply(add, receiver, args)", + ) + .unwrap(); + assert_eq!(result.as_double(), 13.0); +} + +#[test] +fn reflect_construct_throws_clear_type_error() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "try { Reflect.construct(function(){}, []) } catch (e) { e instanceof TypeError }", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn reflect_non_object_target_throws_type_error() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "try { Reflect.get(1, 'x') } catch (e) { e instanceof TypeError }").unwrap(); + assert!(result.as_bool()); +}