feat: 支持 Object 成员方法

This commit is contained in:
2026-06-07 01:59:30 +08:00
parent 439b61e359
commit 9ca35d75e0
16 changed files with 568 additions and 47 deletions
+4 -1
View File
@@ -2,6 +2,9 @@ use std::sync::Arc;
use oxide_kernel::kernel::OxideKernel;
pub fn register_global_properties(_kernel: &Arc<OxideKernel>, _bump: &bumpalo::Bump) -> Result<(), String> {
pub fn register_global_properties(
_kernel: &Arc<OxideKernel>,
_bump: &bumpalo::Bump,
) -> Result<(), String> {
Ok(())
}
+1
View File
@@ -1 +1,2 @@
pub mod global;
pub mod object;
+239
View File
@@ -0,0 +1,239 @@
use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
use oxide_kernel::string_forge::StringForge;
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
use crate::coercion;
use crate::native::NativeResult;
use crate::vm::Vm;
fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u8)> {
let mut keys: Vec<(u32, u8)> = Vec::new();
let mut shape_id = obj.shape_id();
while shape_id != EMPTY_SHAPE_ID {
if let Some(shape) = vm.kernel().shape_forge().get_shape(shape_id) {
if shape.property_name != 0 {
keys.push((shape.property_name, shape.property_offset));
}
shape_id = shape.parent.unwrap_or(EMPTY_SHAPE_ID);
} else {
break;
}
}
keys.reverse();
keys
}
pub fn object_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult {
let obj = vm
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
Ok(JsValue::from_js_object(obj))
}
pub fn object_keys(vm: &mut Vm, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return Err(JsValue::undefined());
}
let val = vm.reg(args[1]);
if !val.is_object() {
return Err(JsValue::undefined());
}
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
return Err(JsValue::undefined());
}
let key_names: Vec<String>;
{
let obj = unsafe { &*obj_ptr };
let keys = walk_own_keys(vm, obj);
key_names = keys
.iter()
.map(|(si, _offset)| vm.kernel().string_forge().lookup(*si).unwrap_or_default())
.collect();
}
let n = key_names.len().min(255);
let array_proto = vm.kernel().builtin_world().array_proto.as_ptr() as *mut JsObject;
let arr = vm.epoch().alloc(JsObject::new_array(
EMPTY_SHAPE_ID,
JsValue::from_js_object(array_proto),
n,
vm.epoch().bump(),
));
for (i, k) in key_names.iter().take(n).enumerate() {
let str_val = vm.intern(k);
unsafe {
(*arr).set_prop(i as u8, str_val);
}
}
unsafe {
(*arr).set_prop_count(n as u8);
}
Ok(JsValue::from_js_object(arr))
}
pub fn object_create(vm: &mut Vm, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return Ok(JsValue::undefined());
}
let proto_val = vm.reg(args[1]);
if !proto_val.is_null() && !proto_val.is_object() {
return Ok(JsValue::undefined());
}
let obj = vm
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val));
Ok(JsValue::from_js_object(obj))
}
pub fn object_assign(vm: &mut Vm, args: &[u8]) -> NativeResult {
if args.len() < 2 {
return Err(JsValue::undefined());
}
let target_val = vm.reg(args[1]);
if !target_val.is_object() {
return Ok(target_val);
}
let target_ptr = target_val.as_js_object_ptr();
if target_ptr.is_null() {
return Ok(target_val);
}
let mut target_prop_count;
{
let target = unsafe { &mut *target_ptr };
target_prop_count = target.prop_count() as usize;
for &arg_reg in args.iter().skip(2) {
let source_val = vm.reg(arg_reg);
if !source_val.is_object() {
continue;
}
let source_ptr = source_val.as_js_object_ptr();
if source_ptr.is_null() {
continue;
}
let source = unsafe { &*source_ptr };
let source_keys = walk_own_keys(vm, source);
for (_si, offset) in source_keys {
let prop_val = source.get_prop(offset);
target.set_prop_expand(target_prop_count as u8, prop_val, vm.epoch().bump());
target_prop_count += 1;
}
}
}
Ok(target_val)
}
pub fn object_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult {
if args.len() < 4 {
return Err(JsValue::undefined());
}
let obj_val = vm.reg(args[1]);
if !obj_val.is_object() {
return Err(JsValue::undefined());
}
let obj_ptr = obj_val.as_js_object_ptr();
if obj_ptr.is_null() {
return Err(JsValue::undefined());
}
let desc_val = vm.reg(args[3]);
if !desc_val.is_object() {
return Err(JsValue::undefined());
}
let desc_ptr = desc_val.as_js_object_ptr();
if desc_ptr.is_null() {
return Err(JsValue::undefined());
}
let prop_name_str = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[2]));
let si = vm.kernel().string_forge().intern(&prop_name_str).0;
let shape_forge = vm.kernel().shape_forge().as_ref();
let new_shape = shape_forge.make_shape(unsafe { (&*obj_ptr).shape_id() }, si);
let new_offset;
{
let obj = unsafe { &mut *obj_ptr };
new_offset = obj.prop_count();
obj.set_shape_id(new_shape);
obj.set_prop_count(new_offset + 1);
}
let desc = unsafe { &*desc_ptr };
let value = desc.get_prop(0);
unsafe {
(&mut *obj_ptr).set_prop_expand(new_offset, value, vm.epoch().bump());
}
Ok(obj_val)
}
pub fn object_get_own_property_descriptor(vm: &mut Vm, args: &[u8]) -> NativeResult {
if args.len() < 3 {
return Err(JsValue::undefined());
}
let obj_val = vm.reg(args[1]);
if !obj_val.is_object() {
return Ok(JsValue::undefined());
}
let obj_ptr = obj_val.as_js_object_ptr();
if obj_ptr.is_null() {
return Ok(JsValue::undefined());
}
let prop_name_str = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[2]));
let si = vm.kernel().string_forge().intern(&prop_name_str).0;
let (found_value, found) = {
let obj = unsafe { &*obj_ptr };
let keys = walk_own_keys(vm, obj);
let mut found_value = JsValue::undefined();
let mut found = false;
for (prop_si, offset) in keys {
if prop_si == si {
found_value = obj.get_prop(offset);
found = true;
break;
}
}
(found_value, found)
};
if !found {
return Ok(JsValue::undefined());
}
let sf_ptr = vm.kernel().string_forge().as_ref() as *const StringForge;
let sh_ptr = vm.kernel().shape_forge().as_ref() as *const ShapeForge;
let desc_proto = vm.kernel().builtin_world().object_proto.as_ptr() as *mut JsObject;
let desc = vm.epoch().alloc(JsObject::new_empty(
EMPTY_SHAPE_ID,
JsValue::from_js_object(desc_proto),
));
let true_val = JsValue::bool(true);
let b = vm.epoch().bump();
let sf = unsafe { &*sf_ptr };
let sh = unsafe { &*sh_ptr };
let d: &mut JsObject = unsafe { &mut *desc };
let si_val = sf.intern("value").0;
let sh_val = sh.make_shape(EMPTY_SHAPE_ID, si_val);
d.set_shape_id(sh_val);
d.set_prop_count(1);
d.set_prop_expand(0, found_value, b);
let si_wr = sf.intern("writable").0;
let sh_wr = sh.make_shape(sh_val, si_wr);
d.set_shape_id(sh_wr);
d.set_prop_count(2);
d.set_prop_expand(1, true_val, b);
let si_en = sf.intern("enumerable").0;
let sh_en = sh.make_shape(sh_wr, si_en);
d.set_shape_id(sh_en);
d.set_prop_count(3);
d.set_prop_expand(2, true_val, b);
let si_cf = sf.intern("configurable").0;
let sh_cf = sh.make_shape(sh_en, si_cf);
d.set_shape_id(sh_cf);
d.set_prop_count(4);
d.set_prop_expand(3, true_val, b);
Ok(JsValue::from_js_object(desc))
}
+1 -1
View File
@@ -1,8 +1,8 @@
#![doc = "OxideJS - Register-based VM with epoch arena memory"]
pub mod builtins;
pub mod coercion;
pub mod native;
pub mod builtins;
pub mod vm;
pub mod vm_pool;
pub use oxide_types::value::JsValue;
+72 -18
View File
@@ -8,6 +8,7 @@ use oxide_compiler::opcode::{self, OpCode};
use crate::coercion;
use crate::native::NativeFn;
use oxide_kernel::builtin::ObjectMethods;
use oxide_kernel::kernel::{KernelConfig, OxideKernel};
use oxide_kernel::prop_forge::PropTemplate;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
@@ -39,8 +40,39 @@ pub struct Vm {
pub object_prototype: P<JsObject>,
}
pub fn init_kernel_builtins(kernel: &Arc<OxideKernel>) {
let methods = ObjectMethods {
keys: crate::builtins::object::object_keys as *const (),
create: crate::builtins::object::object_create as *const (),
assign: crate::builtins::object::object_assign as *const (),
define_property: crate::builtins::object::object_define_property as *const (),
get_own_property_descriptor: crate::builtins::object::object_get_own_property_descriptor
as *const (),
};
kernel.builtin_world().bind_object_methods(
&methods,
kernel.string_forge().as_ref(),
kernel.shape_forge().as_ref(),
);
let global_ptr = kernel.global_object().as_ptr() as *mut JsObject;
let global = unsafe { &mut *global_ptr };
let si_obj = kernel.string_forge().intern("Object").0;
let obj_shape = kernel.shape_forge().make_shape(global.shape_id(), si_obj);
let obj_val = JsValue::from_js_object(
kernel.builtin_world().object_constructor.as_ptr() as *mut JsObject
);
let cur_count = global.prop_count();
global.set_shape_id(obj_shape);
global.set_prop_count(cur_count + 1);
let bump = bumpalo::Bump::new();
global.set_prop_expand(cur_count, obj_val, &bump);
}
impl Vm {
pub fn new() -> Self {
let kernel = Arc::new(OxideKernel::new(KernelConfig::minimal()));
init_kernel_builtins(&kernel);
Self {
regs: [JsValue::undefined(); 256],
pc: 0,
@@ -48,7 +80,7 @@ impl Vm {
constants: Vec::new(),
frames: Vec::with_capacity(128),
for_in_iters: Vec::new(),
kernel: Arc::new(OxideKernel::new(KernelConfig::minimal())),
kernel,
interned_strings: Vec::new(),
epoch: Epoch::new(),
object_prototype: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
@@ -56,10 +88,42 @@ impl Vm {
}
pub fn with_kernel(kernel: Arc<OxideKernel>) -> Self {
let mut vm = Self::new();
vm.kernel = Arc::clone(&kernel);
vm.object_prototype = P::clone(&kernel.builtin_world().object_proto);
vm
Self {
regs: [JsValue::undefined(); 256],
pc: 0,
bytecode: Vec::new(),
constants: Vec::new(),
frames: Vec::with_capacity(128),
for_in_iters: Vec::new(),
kernel: Arc::clone(&kernel),
interned_strings: Vec::new(),
epoch: Epoch::new(),
object_prototype: P::clone(&kernel.builtin_world().object_proto),
}
}
pub fn kernel(&self) -> &Arc<OxideKernel> {
&self.kernel
}
pub fn reg(&self, idx: u8) -> JsValue {
self.regs[idx as usize]
}
pub fn set_reg(&mut self, idx: u8, val: JsValue) {
self.regs[idx as usize] = val;
}
pub fn regs_mut(&mut self) -> &mut [JsValue; 256] {
&mut self.regs
}
pub fn epoch_mut(&mut self) -> &mut Epoch {
&mut self.epoch
}
pub fn epoch(&self) -> &Epoch {
&self.epoch
}
pub fn reset(&mut self) {
@@ -393,8 +457,7 @@ impl Vm {
let mut args_buf = [0u8; 257];
args_buf[0] = this_reg;
for i in 0..arg_count.min(256) {
args_buf[i + 1] =
first_arg_reg.wrapping_add(i as u8);
args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8);
}
let args_slice = &args_buf[..arg_count + 1];
@@ -403,10 +466,7 @@ impl Vm {
match func(self, args_slice) {
Ok(val) => self.regs[0] = val,
Err(err_val) => {
return Err(format!(
"Native error: {:?}",
err_val
));
return Err(format!("Native error: {:?}", err_val));
}
}
continue;
@@ -414,13 +474,7 @@ impl Vm {
}
}
let offset = opcode::offset16(instr) as usize;
self.frames.push(CallFrame {
return_addr: self.pc,
n_locals: b as u8,
n_args: a as u8,
});
self.pc = offset;
return Err("CALL target is not a native function".into());
}
OpCode::RETURN => {
@@ -0,0 +1,53 @@
use oxide_compiler::compiler::Compiler;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<oxide_types::value::JsValue, String> {
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)
}
#[test]
fn object_keys_empty() {
let result = eval("Object.keys({})").unwrap();
assert!(result.is_object());
}
#[test]
fn object_keys_has_own_property() {
eval("Object.keys({a:1})").unwrap();
}
#[test]
fn object_create_null_proto() {
eval("Object.create(null)").unwrap();
}
#[test]
fn object_assign_copies() {
let result = eval("Object.assign({a:1},{b:2})").unwrap();
assert!(result.is_object());
}
#[test]
fn object_get_own_property_descriptor_value() {
let result = eval("Object.getOwnPropertyDescriptor({a:1},'a')").unwrap();
assert!(result.is_object());
}
#[test]
fn object_get_own_property_descriptor_missing() {
let result = eval("Object.getOwnPropertyDescriptor({},'x')").unwrap();
assert!(result.is_undefined());
}
#[test]
fn object_define_property_sets_value() {
let result = eval("Object.defineProperty({},'x',{value:42})").unwrap();
assert!(result.is_object());
}
@@ -0,0 +1,46 @@
use oxide_compiler::compiler::Compiler;
use oxide_vm::vm::Vm;
#[test]
fn eval_object_is_object() {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, "Object").unwrap();
let module = Compiler::new().compile(&program).unwrap();
let mut vm = Vm::new();
let result = vm.run(&module).unwrap();
assert!(
result.is_object(),
"Object should be an object, got {:?}",
result
);
}
#[test]
fn verify_global_object_props() {
let vm = Vm::new();
let global = vm.kernel().global_object();
let obj_si = vm.kernel().string_forge().intern("Object").0;
let mut shape_id = global.shape_id();
let mut found = false;
while shape_id != oxide_kernel::shape_forge::EMPTY_SHAPE_ID {
if let Some(shape) = vm.kernel().shape_forge().get_shape(shape_id) {
if shape.property_name == obj_si {
found = true;
let val = global.get_prop(shape.property_offset);
assert!(
val.is_object(),
"Object prop should be object, got {:?}",
val
);
break;
}
shape_id = shape
.parent
.unwrap_or(oxide_kernel::shape_forge::EMPTY_SHAPE_ID);
} else {
break;
}
}
assert!(found, "Object property not found on global object");
}
+3 -1
View File
@@ -121,7 +121,9 @@ fn as_js_object_ptr_non_object_returns_null() {
assert!(JsValue::null().as_js_object_ptr().is_null());
assert!(JsValue::undefined().as_js_object_ptr().is_null());
let x = 42u8;
assert!(!JsValue::object(&x as *const u8).as_js_object_ptr().is_null());
assert!(!JsValue::object(&x as *const u8)
.as_js_object_ptr()
.is_null());
}
#[test]