diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 9a87a7d3..02fe5850 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -69,7 +69,9 @@ fn main() -> ExitCode { } fn make_kernel() -> Arc { - Arc::new(OxideKernel::new(KernelConfig::standard())) + let kernel = Arc::new(OxideKernel::new(KernelConfig::standard())); + oxide_vm::vm::init_kernel_builtins(&kernel); + kernel } fn make_pool(kernel: &Arc) -> Arc { diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 5db9a89d..ff169a7a 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -91,7 +91,7 @@ impl CompileCtx { } pub(crate) fn reset_regs(&mut self) { - self.next_reg = 0; + self.next_reg = self.builtin_reg_map.len() as u8; self.projected_pc = 0; self.label_counter = 0; } @@ -169,9 +169,22 @@ impl CompileCtx { pub(crate) fn pre_register_builtins(&mut self) { let builtins = [ - "NaN", "undefined", "Object", "Array", "String", "Number", "Boolean", - "Function", "Error", "Math", "JSON", "Promise", "parseInt", "parseFloat", - "isNaN", "isFinite", + "NaN", + "undefined", + "Object", + "Array", + "String", + "Number", + "Boolean", + "Function", + "Error", + "Math", + "JSON", + "Promise", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", ]; for name in &builtins { let reg = self.alloc_reg(); diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 2f3ee20e..db1d89bd 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -198,15 +198,26 @@ impl Compiler { ctx.projected_pc += 1; // NEG/TYPEOF/VOID/NOT } Expression::CallExpression(call) => { - self.count_expression(&call.callee, ctx); - ctx.alloc_reg(); + match &call.callee { + Expression::StaticMemberExpression(member) => { + self.count_expression(&member.object, ctx); + ctx.alloc_reg(); + ctx.projected_pc += 1; + ctx.alloc_reg(); + ctx.projected_pc += 3; + } + _ => { + self.count_expression(&call.callee, ctx); + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + } for arg in &call.arguments { if let Some(expr) = arg.as_expression() { self.count_expression(expr, ctx); } } - ctx.alloc_reg(); - ctx.projected_pc += 2; // CALL + extension word (arg_count) + ctx.projected_pc += 2; } Expression::AssignmentExpression(assign) => { if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 3232e183..58ad1690 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -739,15 +739,37 @@ impl Compiler { } Expression::ParenthesizedExpression(p) => self.emit_expression(&p.expression, ctx), Expression::CallExpression(call) => { - let callee_reg = self.emit_expression(&call.callee, ctx)?; - let this_idx = ctx.add_constant(Constant::Undefined); - let this_reg = ctx.alloc_reg(); - ctx.emit(opcode::encode( - OpCode::LOAD_CONST, - this_reg, - (this_idx & 0xFF) as u8, - ((this_idx >> 8) & 0xFF) as u8, - )); + let (callee_reg, this_reg) = match &call.callee { + Expression::StaticMemberExpression(member) => { + let obj_reg = self.emit_expression(&member.object, ctx)?; + let prop_name = member.property.name.as_str(); + let idx = ctx.add_constant(Constant::String(prop_name.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + key_reg, + (idx & 0xFF) as u8, + ((idx >> 8) & 0xFF) as u8, + )); + let callee_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, callee_reg, obj_reg, 0)); + ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, callee_reg, key_reg)); + ctx.emit(0); + (callee_reg, obj_reg) + } + _ => { + let callee_reg = self.emit_expression(&call.callee, ctx)?; + let this_idx = ctx.add_constant(Constant::Undefined); + let this_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + this_reg, + (this_idx & 0xFF) as u8, + ((this_idx >> 8) & 0xFF) as u8, + )); + (callee_reg, this_reg) + } + }; let mut arg_regs = Vec::new(); for arg in &call.arguments { if let Some(expr) = arg.as_expression() { @@ -759,7 +781,6 @@ impl Compiler { } else { arg_regs[0] }; - let result_reg = ctx.alloc_reg(); ctx.emit(opcode::encode( OpCode::CALL, callee_reg, @@ -767,7 +788,7 @@ impl Compiler { first_arg_reg, )); ctx.emit(arg_regs.len() as u32); - Ok(result_reg) + Ok(0u8) } _ => Err(format!("unsupported expression type: {:?}", expr)), } diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 64d17b84..fe7c4685 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -5,6 +5,14 @@ use oxide_types::value::JsValue; use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use crate::string_forge::StringForge; +pub struct ObjectMethods { + pub keys: *const (), + pub create: *const (), + pub assign: *const (), + pub define_property: *const (), + pub get_own_property_descriptor: *const (), +} + pub struct BuiltinWorld { pub object_proto: P, pub array_proto: P, @@ -152,6 +160,36 @@ impl BuiltinWorld { } } + pub fn bind_object_methods( + &self, + methods: &ObjectMethods, + string_forge: &StringForge, + shape_forge: &ShapeForge, + ) { + let ctor_ptr = P::as_ptr(&self.object_constructor) as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + + let _ = Self::bind_method(ctor, shape_forge, string_forge, "keys", methods.keys, 1); + let _ = Self::bind_method(ctor, shape_forge, string_forge, "create", methods.create, 2); + let _ = Self::bind_method(ctor, shape_forge, string_forge, "assign", methods.assign, 2); + let _ = Self::bind_method( + ctor, + shape_forge, + string_forge, + "defineProperty", + methods.define_property, + 3, + ); + let _ = Self::bind_method( + ctor, + shape_forge, + string_forge, + "getOwnPropertyDescriptor", + methods.get_own_property_descriptor, + 2, + ); + } + pub fn bind_method( proto: &mut JsObject, shape_forge: &ShapeForge, @@ -170,8 +208,7 @@ impl BuiltinWorld { let new_shape = shape_forge.make_shape(proto.shape_id(), si); proto.set_shape_id(new_shape); proto.set_prop_count(new_offset + 1); - let bump = bumpalo::Bump::new(); - proto.set_prop_expand(new_offset, wrapper_val, &bump); + proto.set_prop_expand_heap(new_offset, wrapper_val); proto.bump_generation(); Ok(()) } diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index b67667a2..ed6d986f 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -395,7 +395,10 @@ fn main() { let path_str = path.to_string_lossy(); if path_str.contains("staging") { - results.push(TestResult::skip(path.clone(), "staging tests excluded".into())); + results.push(TestResult::skip( + path.clone(), + "staging tests excluded".into(), + )); stats.skip += 1; continue; } @@ -426,9 +429,21 @@ fn main() { println!("═══════════════════════════════════════"); println!(" total : {}", results.len()); let total = results.len() as f64; - println!(" pass : {} ({:.1}%)", stats.pass, stats.pass as f64 / total * 100.0); - println!(" fail : {} ({:.1}%)", stats.fail, stats.fail as f64 / total * 100.0); - println!(" skip : {} ({:.1}%)", stats.skip, stats.skip as f64 / total * 100.0); + println!( + " pass : {} ({:.1}%)", + stats.pass, + stats.pass as f64 / total * 100.0 + ); + println!( + " fail : {} ({:.1}%)", + stats.fail, + stats.fail as f64 / total * 100.0 + ); + println!( + " skip : {} ({:.1}%)", + stats.skip, + stats.skip as f64 / total * 100.0 + ); println!(" time : {:?}", Duration::from_millis(stats.total_ms)); println!( " pass% : {:.1}% (of ran: {:.1}%)", diff --git a/crates/oxide_types/src/mem.rs b/crates/oxide_types/src/mem.rs index 604fbdc4..8755f058 100644 --- a/crates/oxide_types/src/mem.rs +++ b/crates/oxide_types/src/mem.rs @@ -21,6 +21,10 @@ impl P { { Self(Arc::new(value.clone())) } + + pub fn as_ptr(&self) -> *const T { + Arc::as_ptr(&self.0) + } } impl Clone for P { diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index e1a74e56..764da17e 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -229,6 +229,26 @@ impl JsObject { self.overflow = ptr; ptr } + + pub fn alloc_overflow_heap(&mut self, new_count: usize) -> *mut JsValue { + debug_assert!(new_count > 4); + debug_assert!(new_count <= 32, "overflow limited to 32 slots max"); + let n = new_count - 4; + let v: Vec = vec![JsValue::undefined(); n]; + let ptr = Box::into_raw(v.into_boxed_slice()) as *mut JsValue; + self.overflow = ptr; + ptr + } + + pub fn set_prop_expand_heap(&mut self, offset: u8, val: JsValue) { + if offset >= 4 { + let needed = (offset as usize) + 1; + if self.overflow.is_null() || offset >= self.prop_count() + 4 { + self.alloc_overflow_heap(needed.max(8)); + } + } + self.set_prop(offset, val); + } } #[cfg(test)] diff --git a/crates/oxide_vm/src/builtins/global.rs b/crates/oxide_vm/src/builtins/global.rs index a1b2a563..e9a168f2 100644 --- a/crates/oxide_vm/src/builtins/global.rs +++ b/crates/oxide_vm/src/builtins/global.rs @@ -2,6 +2,9 @@ use std::sync::Arc; use oxide_kernel::kernel::OxideKernel; -pub fn register_global_properties(_kernel: &Arc, _bump: &bumpalo::Bump) -> Result<(), String> { +pub fn register_global_properties( + _kernel: &Arc, + _bump: &bumpalo::Bump, +) -> Result<(), String> { Ok(()) } diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index cdcd83a1..c27707fb 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -1 +1,2 @@ pub mod global; +pub mod object; diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs new file mode 100644 index 00000000..a8c8b0e9 --- /dev/null +++ b/crates/oxide_vm/src/builtins/object.rs @@ -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; + { + 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)) +} diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index d2dfb8cc..13c44a14 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -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; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 40868981..204d5f01 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -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, } +pub fn init_kernel_builtins(kernel: &Arc) { + 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) -> 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 { + &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 => { diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs new file mode 100644 index 00000000..974c1b4e --- /dev/null +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -0,0 +1,53 @@ +use oxide_compiler::compiler::Compiler; +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) +} + +#[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()); +} diff --git a/crates/oxide_vm/tests/debug_object_tests.rs b/crates/oxide_vm/tests/debug_object_tests.rs new file mode 100644 index 00000000..9d2d5621 --- /dev/null +++ b/crates/oxide_vm/tests/debug_object_tests.rs @@ -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"); +} diff --git a/crates/oxide_vm/tests/value_tests.rs b/crates/oxide_vm/tests/value_tests.rs index fbb6877d..710b1bc7 100644 --- a/crates/oxide_vm/tests/value_tests.rs +++ b/crates/oxide_vm/tests/value_tests.rs @@ -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]