diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 173101e..14acf25 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -2,6 +2,70 @@ use crate::value::JsValue; pub type ShapeId = u32; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PropAttributes(pub u8); + +impl PropAttributes { + pub const WRITABLE: u8 = 0b001; + pub const ENUMERABLE: u8 = 0b010; + pub const CONFIGURABLE: u8 = 0b100; + pub const DEFAULT_DATA: Self = Self(Self::WRITABLE | Self::ENUMERABLE | Self::CONFIGURABLE); + + pub const fn new(writable: bool, enumerable: bool, configurable: bool) -> Self { + let mut bits = 0; + if writable { + bits |= Self::WRITABLE; + } + if enumerable { + bits |= Self::ENUMERABLE; + } + if configurable { + bits |= Self::CONFIGURABLE; + } + Self(bits) + } + + pub const fn writable(self) -> bool { + self.0 & Self::WRITABLE != 0 + } + + pub const fn enumerable(self) -> bool { + self.0 & Self::ENUMERABLE != 0 + } + + pub const fn configurable(self) -> bool { + self.0 & Self::CONFIGURABLE != 0 + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PropMetaEntry { + pub attributes: PropAttributes, + pub get: JsValue, + pub set: JsValue, + pub is_accessor: bool, +} + +impl PropMetaEntry { + pub fn data(attributes: PropAttributes) -> Self { + Self { + attributes, + get: JsValue::undefined(), + set: JsValue::undefined(), + is_accessor: false, + } + } + + pub fn accessor(get: JsValue, set: JsValue, attributes: PropAttributes) -> Self { + Self { + attributes, + get, + set, + is_accessor: true, + } + } +} + pub trait PropIndex { fn to_u32(self) -> u32; } @@ -48,6 +112,7 @@ impl PropIndex for i32 { /// [31] is_function /// native_arg_count: u8 (1 byte + 3 pad) /// hash_props: *mut u8 (8 bytes, points to Box>>) +/// prop_meta: *mut u8 (8 bytes, points to Box>>) /// proto: JsValue (8 bytes) /// generation: u32 (4 bytes + 4 pad) /// native_fn: u64 (8 bytes, 0 = None sentinel) @@ -60,6 +125,7 @@ pub struct JsObject { native_arg_count: u8, _pad: [u8; 3], hash_props: *mut u8, + prop_meta: *mut u8, proto: JsValue, generation: u32, _pad2: [u8; 4], @@ -77,6 +143,7 @@ impl JsObject { native_arg_count: 0, _pad: [0; 3], hash_props: std::ptr::null_mut(), + prop_meta: std::ptr::null_mut(), proto, generation: 1, _pad2: [0; 4], @@ -94,6 +161,7 @@ impl JsObject { native_arg_count: 0, _pad: [0; 3], hash_props: std::ptr::null_mut(), + prop_meta: std::ptr::null_mut(), proto, generation: 1, _pad2: [0; 4], @@ -129,15 +197,87 @@ impl JsObject { /// Sets the length of hash_props vec. Truncates or extends with undefined. pub fn set_prop_count(&mut self, count: impl PropIndex) { - let vec = self.ensure_hash_props(); let target = count.to_u32() as usize; - if target < vec.len() { - vec.truncate(target); - } else { - while vec.len() < target { - vec.push(Box::new(JsValue::undefined())); + { + let vec = self.ensure_hash_props(); + if target < vec.len() { + vec.truncate(target); + } else { + while vec.len() < target { + vec.push(Box::new(JsValue::undefined())); + } } } + if let Some(meta) = self.prop_meta_vec_mut() { + if target < meta.len() { + meta.truncate(target); + } else { + while meta.len() < target { + meta.push(None); + } + } + } + } + + pub fn has_prop_meta(&self) -> bool { + !self.prop_meta.is_null() + } + + pub fn ensure_prop_meta(&mut self) -> &mut Vec> { + if self.prop_meta.is_null() { + let len = self.prop_vec_len(); + let vec = Box::new(vec![None::; len]); + self.prop_meta = Box::into_raw(vec) as *mut u8; + } + unsafe { &mut *(self.prop_meta as *mut Vec>) } + } + + pub fn prop_meta_vec(&self) -> Option<&Vec>> { + if self.prop_meta.is_null() { + None + } else { + unsafe { Some(&*(self.prop_meta as *const Vec>)) } + } + } + + fn prop_meta_vec_mut(&mut self) -> Option<&mut Vec>> { + if self.prop_meta.is_null() { + None + } else { + unsafe { Some(&mut *(self.prop_meta as *mut Vec>)) } + } + } + + pub fn prop_meta_at(&self, position: impl PropIndex) -> Option { + let pos = position.to_u32() as usize; + self.prop_meta_vec().and_then(|vec| vec.get(pos).copied().flatten()) + } + + pub fn set_data_meta(&mut self, position: impl PropIndex, attributes: PropAttributes) { + self.set_meta_at(position, PropMetaEntry::data(attributes)); + } + + pub fn set_accessor_meta( + &mut self, position: impl PropIndex, get: JsValue, set: JsValue, attributes: PropAttributes, + ) { + self.set_meta_at(position, PropMetaEntry::accessor(get, set, attributes)); + } + + pub fn is_accessor_meta(&self, position: impl PropIndex) -> bool { + self.prop_meta_at(position).is_some_and(|entry| entry.is_accessor) + } + + fn set_meta_at(&mut self, position: impl PropIndex, entry: PropMetaEntry) { + let pos = position.to_u32() as usize; + let prop_len = self.prop_vec_len(); + if pos >= prop_len { + self.set_prop_count(pos + 1); + } + let meta = self.ensure_prop_meta(); + while meta.len() <= pos { + meta.push(None); + } + meta[pos] = Some(entry); } pub fn is_array(&self) -> bool { @@ -198,15 +338,22 @@ impl JsObject { /// Set property value at position index. Vec auto-grows if needed. pub fn set_prop_at(&mut self, position: impl PropIndex, val: JsValue) { - let vec = self.ensure_hash_props(); let pos = position.to_u32() as usize; - if pos < vec.len() { - *vec[pos] = val; - } else { - while vec.len() < pos { - vec.push(Box::new(JsValue::undefined())); + { + let vec = self.ensure_hash_props(); + if pos < vec.len() { + *vec[pos] = val; + } else { + while vec.len() < pos { + vec.push(Box::new(JsValue::undefined())); + } + vec.push(Box::new(val)); + } + } + if let Some(meta) = self.prop_meta_vec_mut() { + while meta.len() <= pos { + meta.push(None); } - vec.push(Box::new(val)); } } @@ -215,6 +362,9 @@ impl JsObject { let vec = self.ensure_hash_props(); let pos = vec.len(); vec.push(Box::new(val)); + if let Some(meta) = self.prop_meta_vec_mut() { + meta.push(None); + } pos as u32 } @@ -381,6 +531,7 @@ mod tests { assert!(!obj.is_function()); assert_eq!(obj.generation(), 1); assert!(obj.hash_props_vec().is_none()); + assert!(!obj.has_prop_meta()); } #[test] @@ -452,4 +603,38 @@ mod tests { assert_eq!(obj.get_prop_at(0), JsValue::int(100)); assert_eq!(obj.get_prop_at(1), JsValue::int(200)); } + + #[test] + fn prop_meta_lazy_init() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_at(0, JsValue::int(1)); + assert!(!obj.has_prop_meta()); + + obj.set_data_meta(0, PropAttributes::new(false, true, false)); + assert!(obj.has_prop_meta()); + let meta = obj.prop_meta_at(0).expect("meta"); + assert!(!meta.is_accessor); + assert!(!meta.attributes.writable()); + assert!(meta.attributes.enumerable()); + assert!(!meta.attributes.configurable()); + } + + #[test] + fn accessor_meta_roundtrip_and_alignment() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_at(0, JsValue::int(1)); + obj.set_accessor_meta(2, JsValue::int(10), JsValue::int(11), PropAttributes::new(false, false, true)); + + assert_eq!(obj.prop_count(), 3); + assert!(obj.is_accessor_meta(2)); + let meta = obj.prop_meta_at(2).expect("accessor meta"); + assert_eq!(meta.get, JsValue::int(10)); + assert_eq!(meta.set, JsValue::int(11)); + assert!(!meta.attributes.writable()); + assert!(!meta.attributes.enumerable()); + assert!(meta.attributes.configurable()); + + obj.push_prop(JsValue::int(4)); + assert_eq!(obj.prop_meta_vec().expect("meta").len(), obj.prop_vec_len()); + } } diff --git a/crates/oxide_vm/src/dispatch/member.rs b/crates/oxide_vm/src/dispatch/member.rs index 4b31e24..3809d13 100644 --- a/crates/oxide_vm/src/dispatch/member.rs +++ b/crates/oxide_vm/src/dispatch/member.rs @@ -46,11 +46,12 @@ impl Vm { let Ok((obj_ptr, prop_name_si)) = self.member_target(rd, b, "MEMBER_INC on non-object") else { return Ok(()); }; + let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si); + let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); let new_val = JsValue::float(n + 1.0); - self.set_member_prop(obj, prop_name_si, new_val)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver)?; self.regs[a] = new_val; Ok(()) } @@ -59,11 +60,12 @@ impl Vm { let Ok((obj_ptr, prop_name_si)) = self.member_target(rd, b, "MEMBER_DEC on non-object") else { return Ok(()); }; + let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si); + let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); let new_val = JsValue::float(n - 1.0); - self.set_member_prop(obj, prop_name_si, new_val)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver)?; self.regs[a] = new_val; Ok(()) } @@ -74,12 +76,13 @@ impl Vm { self.raise_type_error("DYN_MEMBER_INC on non-object")?; return Ok(()); } + let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); - let prop_val = self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()); + let prop_val = self.ordinary_get(obj, prop_name_si, receiver)?; let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); let new_val = JsValue::float(n + 1.0); - self.set_or_create_prop_value(obj, prop_name_si, new_val); + self.ordinary_set(obj, prop_name_si, new_val, receiver)?; self.regs[b] = new_val; Ok(()) } @@ -90,12 +93,13 @@ impl Vm { self.raise_type_error("DYN_MEMBER_DEC on non-object")?; return Ok(()); } + let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); - let prop_val = self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()); + let prop_val = self.ordinary_get(obj, prop_name_si, receiver)?; let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); let new_val = JsValue::float(n - 1.0); - self.set_or_create_prop_value(obj, prop_name_si, new_val); + self.ordinary_set(obj, prop_name_si, new_val, receiver)?; self.regs[b] = new_val; Ok(()) } @@ -104,8 +108,9 @@ impl Vm { let Ok((obj_ptr, prop_name_si)) = self.member_target(rd, b, "COMPOUND_MEMBER_ADD on non-object") else { return Ok(()); }; + let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si); + let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; let rhs = self.regs[a]; let new_val = if prop_val.is_string() || rhs.is_string() { let ls = coercion::to_string(self.kernel.string_forge().as_ref(), prop_val); @@ -116,7 +121,7 @@ impl Vm { let rn = coercion::to_number(rhs, self.kernel.string_forge().as_ref()); JsValue::float(ln + rn) }; - self.set_member_prop(obj, prop_name_si, new_val)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver)?; self.regs[a] = new_val; Ok(()) } @@ -130,13 +135,14 @@ impl Vm { let Ok((obj_ptr, prop_name_si)) = self.member_target(rd, b, error_msg) else { return Ok(()); }; + let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si); + let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); let rn = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); let new_val = JsValue::float(op(ln, rn)); self.regs[a] = new_val; - self.set_member_prop(obj, prop_name_si, new_val)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver)?; Ok(()) } } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 5d9c890..f41deca 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -27,7 +27,8 @@ impl Vm { let proto_ptr = self.kernel.builtin_world().string_proto.as_ptr() as *mut JsObject; let proto = unsafe { &*proto_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); - if let Some(resolved) = self.resolve_property(proto, prop_name_si) { + let resolved = self.ordinary_get(proto, prop_name_si, val)?; + if !resolved.is_undefined() { self.regs[a] = resolved; self.pc += 3; return Ok(()); @@ -37,7 +38,8 @@ impl Vm { let proto_ptr = self.kernel.builtin_world().number_proto.as_ptr() as *mut JsObject; let proto = unsafe { &*proto_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); - if let Some(resolved) = self.resolve_property(proto, prop_name_si) { + let resolved = self.ordinary_get(proto, prop_name_si, val)?; + if !resolved.is_undefined() { self.regs[a] = resolved; self.pc += 3; return Ok(()); @@ -53,6 +55,10 @@ impl Vm { let ext1 = self.bytecode[self.pc + 1]; let ext2 = self.bytecode[self.pc + 2]; self.pc += 3; + if obj.has_prop_meta() { + self.regs[a] = self.ordinary_get(obj, prop_name_si, val)?; + return Ok(()); + } let cached_shape_id = ext0 & 0x00FF_FFFF; let cached_ptr = ((ext2 as u64) << 32) | (ext1 as u64); @@ -64,20 +70,19 @@ impl Vm { self.write_ic_back(obj.shape_id(), ptr); self.regs[a] = unsafe { *ptr }; } else { - self.regs[a] = self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()); + self.regs[a] = self.ordinary_get(obj, prop_name_si, val)?; } } else { - self.regs[a] = self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()); + self.regs[a] = self.ordinary_get(obj, prop_name_si, val)?; } - } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + } else { + let resolved = self.ordinary_get(obj, prop_name_si, val)?; if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { if let Some(ptr) = obj.prop_ptr_at(pos) { self.write_ic_back(obj.shape_id(), ptr); } } - self.regs[a] = val; - } else { - self.regs[a] = JsValue::undefined(); + self.regs[a] = resolved; } Ok(()) @@ -103,22 +108,28 @@ impl Vm { let ext1 = self.bytecode[self.pc + 1]; let ext2 = self.bytecode[self.pc + 2]; self.pc += 3; + let value = self.regs[a]; + let receiver = self.regs[rd]; + if obj.has_prop_meta() { + self.ordinary_set(obj, prop_name_si, value, receiver)?; + return Ok(()); + } let cached_shape_id = ext0 & 0x00FF_FFFF; let cached_ptr = ((ext2 as u64) << 32) | (ext1 as u64); if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_ptr != 0 { unsafe { - *(cached_ptr as *mut JsValue) = self.regs[a]; + *(cached_ptr as *mut JsValue) = value; } } else if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - obj.set_prop_at(pos, self.regs[a]); + obj.set_prop_at(pos, value); if let Some(ptr) = obj.prop_ptr_at(pos) { self.write_ic_back(obj.shape_id(), ptr); } } else { let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); obj.set_shape_id(new_shape_id); - let new_pos = obj.push_prop(self.regs[a]); + let new_pos = obj.push_prop(value); obj.bump_generation(); if let Some(ptr) = obj.prop_ptr_at(new_pos) { self.write_ic_back(new_shape_id, ptr); @@ -145,7 +156,7 @@ impl Vm { } let obj = unsafe { &*obj_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); - self.regs[a] = self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()); + self.regs[a] = self.ordinary_get(obj, prop_name_si, self.regs[rd])?; Ok(()) } @@ -161,14 +172,7 @@ impl Vm { obj.set_proto(self.regs[a]).map_err(|e| e.to_string())?; return Ok(()); } - if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - obj.set_prop_at(pos, self.regs[a]); - } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - obj.push_prop(self.regs[a]); - obj.bump_generation(); - } + self.ordinary_set(obj, prop_name_si, self.regs[a], self.regs[rd])?; Ok(()) } @@ -180,7 +184,7 @@ impl Vm { } let obj = unsafe { &*obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); - self.regs[b] = self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()); + self.regs[b] = self.ordinary_get(obj, prop_name_si, self.regs[rd])?; Ok(()) } @@ -196,14 +200,7 @@ impl Vm { obj.set_proto(self.regs[b]).map_err(|e| e.to_string())?; return Ok(()); } - if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - obj.set_prop_at(pos, self.regs[b]); - } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - obj.push_prop(self.regs[b]); - obj.bump_generation(); - } + self.ordinary_set(obj, prop_name_si, self.regs[b], self.regs[rd])?; Ok(()) } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 37295f0..fcaf59a 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -427,63 +427,264 @@ impl Vm { None } - pub(crate) fn read_member_prop(&mut self, obj: &JsObject, prop_name_si: u32) -> JsValue { + pub(crate) fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option { + let length_si = self.kernel.string_forge().intern("length").0; + if obj.is_array() && prop_name_si == length_si { + return None; + } + self.kernel + .shape_forge() + .lookup_position(obj.shape_id(), prop_name_si) + .and_then(|pos| { + let val = obj.get_prop_at(pos); + if !val.is_undefined() || obj.prop_vec_len() > pos as usize { + Some(pos) + } else { + None + } + }) + } + + fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result { + if !callee.is_object() { + return Err("TypeError: accessor is not callable".into()); + } + let callee_obj = unsafe { &*callee.as_js_object_ptr() }; + if !callee_obj.is_function() { + return Err("TypeError: accessor is not callable".into()); + } + + if let Some(native_fn) = callee_obj.native_fn() { + let saved_regs = self.regs; + let saved_this = self.regs[254]; + let saved_callee = self.regs[253]; + self.regs[253] = receiver; + for (idx, arg) in args.iter().enumerate() { + self.regs[240 + idx] = *arg; + } + let mut arg_regs = [0u8; 17]; + arg_regs[0] = 253; + for idx in 0..args.len().min(16) { + arg_regs[idx + 1] = 240 + idx as u8; + } + let func: NativeFn = unsafe { std::mem::transmute(native_fn) }; + let result = func(self, &arg_regs[..args.len().min(16) + 1]); + self.regs = saved_regs; + self.regs[254] = saved_this; + self.regs[253] = saved_callee; + return result.map_err(|err| self.error_text(err)); + } + + if callee_obj.sub_module_index() == 0 { + return Err("TypeError: accessor is not callable".into()); + } + let sub_idx = callee_obj.sub_module_index() as usize - 1; + if sub_idx >= self.sub_modules.len() { + return Err(format!( + "accessor sub_module_index {} out of bounds (max {})", + sub_idx, + self.sub_modules.len() + )); + } + if self.frames.len() >= self.kernel.config.max_call_depth { + return Err("RangeError: Maximum call stack size exceeded".into()); + } + + let saved_regs = self.regs; + let saved_pc = self.pc; + let saved_bytecode = std::mem::take(&mut self.bytecode); + let saved_constants = std::mem::take(&mut self.constants); + let saved_frames = std::mem::take(&mut self.frames); + let saved_bytecode_stack = std::mem::take(&mut self.saved_bytecode_stack); + let saved_constants_stack = std::mem::take(&mut self.saved_constants_stack); + let saved_active_reg_limit = self.active_reg_limit; + let saved_exception_value = self.exception_value.take(); + let saved_pending_exception = self.pending_exception.take(); + let saved_pending_error_kind = self.pending_error_kind.take(); + + let sub = self.sub_modules[sub_idx].clone(); + let sub_param_base = sub.param_base as usize; + let sub_arg_count = sub.n_args as usize; + for i in 0..sub_arg_count { + self.regs[sub_param_base + i] = args.get(i).copied().unwrap_or(JsValue::undefined()); + } + self.regs[254] = if sub.is_arrow { callee_obj.captured_this() } else { receiver }; + self.regs[255] = JsValue::undefined(); + self.bytecode = sub.bytecode; + let converted_constants = match self.convert_constants(&sub.constants) { + Ok(constants) => constants, + Err(err) => { + self.regs = saved_regs; + self.pc = saved_pc; + self.bytecode = saved_bytecode; + self.constants = saved_constants; + self.frames = saved_frames; + self.saved_bytecode_stack = saved_bytecode_stack; + self.saved_constants_stack = saved_constants_stack; + self.active_reg_limit = saved_active_reg_limit; + self.exception_value = saved_exception_value; + self.pending_exception = saved_pending_exception; + self.pending_error_kind = saved_pending_error_kind; + return Err(err); + } + }; + self.constants = converted_constants; + for (name, reg) in &sub.builtin_reg_map { + let si = self.kernel.string_forge().intern(name.as_str()).0; + let global = self.kernel.global_object(); + if let Some(pos) = self.kernel.shape_forge().lookup_position(global.shape_id(), si) { + self.regs[*reg as usize] = global.get_prop_at(pos); + } + } + self.active_reg_limit = sub.n_registers.max(1); + self.pc = 0; + + let result = self.dispatch(); + + self.regs = saved_regs; + self.pc = saved_pc; + self.bytecode = saved_bytecode; + self.constants = saved_constants; + self.frames = saved_frames; + self.saved_bytecode_stack = saved_bytecode_stack; + self.saved_constants_stack = saved_constants_stack; + self.active_reg_limit = saved_active_reg_limit; + self.exception_value = saved_exception_value; + self.pending_exception = saved_pending_exception; + self.pending_error_kind = saved_pending_error_kind; + + result + } + + pub(crate) fn ordinary_get( + &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, + ) -> Result { + let length_si = self.kernel.string_forge().intern("length").0; + if obj.is_array() && prop_name_si == length_si { + return Ok(JsValue::int(obj.prop_count() as i32)); + } + if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { + if let Some(meta) = obj.prop_meta_at(pos) { + if meta.is_accessor { + return if meta.get.is_undefined() { + Ok(JsValue::undefined()) + } else { + self.call_function_sync(meta.get, receiver, &[]) + }; + } + } + return Ok(obj.get_prop_at(pos)); + } + let proto = obj.proto(); + if proto.is_object() { + let proto_obj = unsafe { &*proto.as_js_object_ptr() }; + return self.ordinary_get(proto_obj, prop_name_si, receiver); + } + Ok(JsValue::undefined()) + } + + fn inherited_property_meta(&self, obj: &JsObject, prop_name_si: u32) -> Option { + let mut proto = obj.proto(); + while proto.is_object() { + let proto_obj = unsafe { &*proto.as_js_object_ptr() }; + if let Some(pos) = self.get_own_property_slot(proto_obj, prop_name_si) { + return proto_obj.prop_meta_at(pos); + } + proto = proto_obj.proto(); + } + None + } + + pub(crate) fn ordinary_set( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + ) -> Result<(), String> { + if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { + 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"); + } + self.call_function_sync(meta.set, receiver, &[val])?; + return Ok(()); + } + if !meta.attributes.writable() { + return self.raise_type_error("cannot assign to read-only property"); + } + } + obj.set_prop_at(pos, val); + return Ok(()); + } + + 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"); + } + self.call_function_sync(meta.set, receiver, &[val])?; + return Ok(()); + } + if !meta.attributes.writable() { + return self.raise_type_error("cannot assign to read-only property"); + } + } + + self.set_or_create_prop_value(obj, prop_name_si, val); + Ok(()) + } + + pub(crate) fn read_member_prop( + &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, + ) -> Result { let ext0 = self.bytecode[self.pc]; let ext1 = self.bytecode[self.pc + 1]; let ext2 = self.bytecode[self.pc + 2]; self.pc += 3; + if obj.has_prop_meta() { + return self.ordinary_get(obj, prop_name_si, receiver); + } let cached_shape_id = ext0 & 0x00FF_FFFF; let cached_ptr = ((ext2 as u64) << 32) | (ext1 as u64); - if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_ptr != 0 { + let val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() && cached_ptr != 0 { unsafe { *(cached_ptr as *const JsValue) } } else if let Some(template) = self.kernel.prop_forge().get_template(obj.shape_id()) { if template.prop_name != prop_name_si { - self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()) + self.ordinary_get(obj, prop_name_si, receiver)? } else if let Some(ptr) = self.template_prop_ptr(obj, &template) { self.write_ic_back(obj.shape_id(), ptr); unsafe { *ptr } } else { - self.resolve_property(obj, prop_name_si).unwrap_or(JsValue::undefined()) + self.ordinary_get(obj, prop_name_si, receiver)? } - } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + } else { + let resolved = self.ordinary_get(obj, prop_name_si, receiver)?; if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - if let Some(ptr) = obj.prop_ptr_at(pos) { - self.write_ic_back(obj.shape_id(), ptr); + if !obj.is_accessor_meta(pos) { + if let Some(ptr) = obj.prop_ptr_at(pos) { + self.write_ic_back(obj.shape_id(), ptr); + } } } - val - } else { - JsValue::undefined() - } + resolved + }; + Ok(val) } pub(crate) fn set_member_prop( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, ) -> Result<(), String> { if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + if obj.has_prop_meta() { + self.ordinary_set(obj, prop_name_si, val, receiver)?; + return Ok(()); + } obj.set_prop_at(pos, val); // IC write-back: 3 extension words (shape_id + ptr_lo + ptr_hi) if let Some(ptr) = obj.prop_ptr_at(pos) { self.write_ic_back(obj.shape_id(), ptr); } } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - let new_pos = obj.push_prop(val); - obj.bump_generation(); - if let Some(ptr) = obj.prop_ptr_at(new_pos) { - self.write_ic_back(new_shape_id, ptr); - self.kernel.prop_forge().upsert( - new_shape_id, - PropTemplate { - shape_id: new_shape_id, - prop_name: prop_name_si, - position: new_pos, - generation: obj.generation(), - }, - ); - } + self.ordinary_set(obj, prop_name_si, val, receiver)?; } Ok(()) } @@ -1648,7 +1849,65 @@ impl Default for Vm { #[cfg(test)] mod tests { use super::{opcode, JsValue, TryHandler, Vm}; + use crate::native::NativeResult; use oxide_compiler::module::{CompiledModule, Constant}; + use oxide_types::object::{JsObject, PropAttributes}; + + fn native_return_7(_vm: &mut Vm, _args: &[u8]) -> NativeResult { + Ok(JsValue::int(7)) + } + + fn native_get_marker(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(args[0]); + if !this_val.is_object() { + return Ok(JsValue::undefined()); + } + let marker_si = vm.kernel.string_forge().intern("marker").0; + let obj = unsafe { &*this_val.as_js_object_ptr() }; + Ok(vm.resolve_property(obj, marker_si).unwrap_or(JsValue::undefined())) + } + + fn native_set_marker(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(args[0]); + let value = vm.reg(args[1]); + if !this_val.is_object() { + return Ok(JsValue::undefined()); + } + let marker_si = vm.kernel.string_forge().intern("marker").0; + let obj = unsafe { &mut *this_val.as_js_object_ptr() }; + vm.set_or_create_prop_value(obj, marker_si, value); + Ok(JsValue::undefined()) + } + + fn native_function(vm: &mut Vm, f: crate::native::NativeFn) -> JsValue { + let proto = vm.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject; + let mut obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto)); + obj.set_function(true); + obj.set_native_fn(Some(f as *const ())); + JsValue::object(vm.epoch.alloc(obj) as *mut u8) + } + + fn plain_object(vm: &mut Vm) -> JsValue { + let proto = vm.kernel.builtin_world().object_proto.as_ptr() as *mut JsObject; + let obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto)); + JsValue::object(vm.epoch.alloc(obj) as *mut u8) + } + + fn add_accessor(vm: &mut Vm, obj_val: JsValue, name: &str, get: JsValue, set: JsValue) { + let si = vm.kernel.string_forge().intern(name).0; + let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; + let shape_id = vm.kernel.shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(shape_id); + let pos = obj.push_prop(JsValue::undefined()); + obj.set_accessor_meta(pos, get, set, PropAttributes::DEFAULT_DATA); + obj.bump_generation(); + } + + fn set_data(vm: &mut Vm, obj_val: JsValue, name: &str, val: JsValue) { + let si = vm.kernel.string_forge().intern(name).0; + let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; + vm.set_or_create_prop_value(obj, si, val); + } #[test] fn reset_clears_runtime_state_like_rerun() { @@ -1784,4 +2043,109 @@ mod tests { let err = vm.run(&module).expect_err("unimplemented opcode should fail explicitly"); assert_eq!(err, "opcode PROFILE_TYPE not yet implemented"); } + + #[test] + fn ordinary_get_calls_own_native_getter() { + let mut vm = Vm::new(); + let obj_val = plain_object(&mut vm); + let getter = native_function(&mut vm, native_return_7); + add_accessor(&mut vm, obj_val, "x", getter, JsValue::undefined()); + + let x_si = vm.kernel.string_forge().intern("x").0; + let obj = unsafe { &*obj_val.as_js_object_ptr() }; + let value = vm.ordinary_get(obj, x_si, obj_val).expect("getter"); + assert_eq!(value, JsValue::int(7)); + } + + #[test] + fn ordinary_set_calls_own_native_setter() { + let mut vm = Vm::new(); + let obj_val = plain_object(&mut vm); + let setter = native_function(&mut vm, native_set_marker); + add_accessor(&mut vm, obj_val, "x", JsValue::undefined(), setter); + + let x_si = vm.kernel.string_forge().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"); + + let marker_si = vm.kernel.string_forge().intern("marker").0; + let obj = unsafe { &*obj_val.as_js_object_ptr() }; + assert_eq!(vm.resolve_property(obj, marker_si), Some(JsValue::int(9))); + } + + #[test] + fn inherited_getter_uses_original_receiver() { + let mut vm = Vm::new(); + let proto_val = plain_object(&mut vm); + let child_val = plain_object(&mut vm); + let getter = native_function(&mut vm, native_get_marker); + add_accessor(&mut vm, proto_val, "x", getter, JsValue::undefined()); + set_data(&mut vm, child_val, "marker", JsValue::int(42)); + unsafe { + (*child_val.as_js_object_ptr()).set_proto(proto_val).expect("proto"); + } + + let x_si = vm.kernel.string_forge().intern("x").0; + let child = unsafe { &*child_val.as_js_object_ptr() }; + let value = vm.ordinary_get(child, x_si, child_val).expect("getter"); + assert_eq!(value, JsValue::int(42)); + } + + #[test] + fn inherited_setter_uses_original_receiver() { + let mut vm = Vm::new(); + let proto_val = plain_object(&mut vm); + let child_val = plain_object(&mut vm); + let setter = native_function(&mut vm, native_set_marker); + add_accessor(&mut vm, proto_val, "x", JsValue::undefined(), setter); + unsafe { + (*child_val.as_js_object_ptr()).set_proto(proto_val).expect("proto"); + } + + let x_si = vm.kernel.string_forge().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"); + + let marker_si = vm.kernel.string_forge().intern("marker").0; + let child = unsafe { &*child_val.as_js_object_ptr() }; + let proto = unsafe { &*proto_val.as_js_object_ptr() }; + assert_eq!(vm.resolve_property(child, marker_si), Some(JsValue::int(12))); + assert_eq!(vm.resolve_property(proto, marker_si), None); + } + + #[test] + fn deep_inherited_setter_uses_original_receiver() { + let mut vm = Vm::new(); + let grand_proto_val = plain_object(&mut vm); + let proto_val = plain_object(&mut vm); + let child_val = plain_object(&mut vm); + let setter = native_function(&mut vm, native_set_marker); + add_accessor(&mut vm, grand_proto_val, "x", JsValue::undefined(), setter); + unsafe { + (*proto_val.as_js_object_ptr()).set_proto(grand_proto_val).expect("proto"); + (*child_val.as_js_object_ptr()).set_proto(proto_val).expect("proto"); + } + + let x_si = vm.kernel.string_forge().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"); + + let marker_si = vm.kernel.string_forge().intern("marker").0; + let child = unsafe { &*child_val.as_js_object_ptr() }; + assert_eq!(vm.resolve_property(child, marker_si), Some(JsValue::int(15))); + } + + #[test] + fn ordinary_data_property_still_reads_and_writes_without_meta() { + let mut vm = Vm::new(); + let obj_val = plain_object(&mut vm); + set_data(&mut vm, obj_val, "x", JsValue::int(1)); + + let x_si = vm.kernel.string_forge().intern("x").0; + 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"); + assert_eq!(vm.ordinary_get(obj, x_si, obj_val).expect("get"), JsValue::int(2)); + } }