diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index 8e421785..31a3e6eb 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -2,6 +2,7 @@ pub mod coercion; pub mod mem; +pub mod object; pub mod value; pub mod vm; pub use value::JsValue; diff --git a/crates/oxide_vm/src/object.rs b/crates/oxide_vm/src/object.rs new file mode 100644 index 00000000..365992bd --- /dev/null +++ b/crates/oxide_vm/src/object.rs @@ -0,0 +1,245 @@ +use crate::value::JsValue; + +pub type ShapeId = u32; + +pub const EMPTY_SHAPE_ID: ShapeId = 0; + +/// Layout: +/// header: u32 bits +/// [0:23] shape_id +/// [24:28] prop_count (5 bits, 0-31) +/// [29] is_array +/// [30] is_extensible +/// [31] is_function +/// inline[0..=3]: 4 x JsValue (32 bytes) +/// overflow: *mut JsValue (8 bytes, null if ≤4 properties) +/// proto: JsValue (8 bytes) +/// generation: u32 (4 bytes) +/// _pad: u32 (4 bytes, 64B total = one cache line) +#[repr(C)] +pub struct JsObject { + header: u32, + inline: [JsValue; 4], + overflow: *mut JsValue, + proto: JsValue, + generation: u32, + _pad: u32, +} + +impl JsObject { + pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self { + Self { + header: (shape_id & 0x00FF_FFFF) | (1 << 30), + inline: [JsValue::undefined(); 4], + overflow: std::ptr::null_mut(), + proto, + generation: 1, + _pad: 0, + } + } + + pub fn new_array(shape_id: ShapeId, proto: JsValue, n_elements: usize) -> Self { + assert!( + n_elements <= 4, + "arrays with >4 inline elements not yet supported" + ); + Self { + header: (shape_id & 0x00FF_FFFF) | (1 << 30) | (1 << 29), + inline: [JsValue::undefined(); 4], + overflow: std::ptr::null_mut(), + proto, + generation: 1, + _pad: 0, + } + } + + pub fn shape_id(&self) -> ShapeId { + self.header & 0x00FF_FFFF + } + + pub fn set_shape_id(&mut self, id: ShapeId) { + self.header = (self.header & !0x00FF_FFFF) | (id & 0x00FF_FFFF); + } + + pub fn prop_count(&self) -> u8 { + ((self.header >> 24) & 0x1F) as u8 + } + + pub fn set_prop_count(&mut self, count: u8) { + let count = count.min(31); + self.header = (self.header & !(0x1F << 24)) | ((count as u32) << 24); + } + + pub fn is_array(&self) -> bool { + (self.header >> 29) & 1 != 0 + } + + pub fn is_extensible(&self) -> bool { + (self.header >> 30) & 1 != 0 + } + + pub fn set_extensible(&mut self, ext: bool) { + if ext { + self.header |= 1 << 30; + } else { + self.header &= !(1 << 30); + } + } + + pub fn is_function(&self) -> bool { + (self.header >> 31) & 1 != 0 + } + + pub fn set_function(&mut self, f: bool) { + if f { + self.header |= 1 << 31; + } else { + self.header &= !(1 << 31); + } + } + + pub fn get_inline_prop(&self, offset: u8) -> JsValue { + debug_assert!(offset < 4, "inline offset out of range"); + self.inline[offset as usize] + } + + pub fn set_inline_prop(&mut self, offset: u8, val: JsValue) { + debug_assert!(offset < 4, "inline offset out of range"); + self.inline[offset as usize] = val; + } + + pub fn get_overflow_prop(&self, offset: u8) -> JsValue { + debug_assert!(offset >= 4); + if self.overflow.is_null() { + return JsValue::undefined(); + } + unsafe { *self.overflow.add((offset - 4) as usize) } + } + + pub fn set_overflow_prop(&mut self, offset: u8, val: JsValue) { + debug_assert!(offset >= 4); + debug_assert!(!self.overflow.is_null(), "overflow buffer not allocated"); + unsafe { + *self.overflow.add((offset - 4) as usize) = val; + } + } + + pub fn get_prop(&self, offset: u8) -> JsValue { + if offset < 4 { + self.get_inline_prop(offset) + } else { + self.get_overflow_prop(offset) + } + } + + pub fn set_prop(&mut self, offset: u8, val: JsValue) { + if offset < 4 { + self.set_inline_prop(offset, val); + } else { + self.set_overflow_prop(offset, val); + } + } + + pub fn proto(&self) -> JsValue { + self.proto + } + + pub fn set_proto(&mut self, proto: JsValue) { + self.proto = proto; + self.generation = self.generation.wrapping_add(1); + } + + pub fn generation(&self) -> u32 { + self.generation + } + + pub fn bump_generation(&mut self) { + self.generation = self.generation.wrapping_add(1); + } + + pub fn alloc_overflow(&mut self, _bump: &bumpalo::Bump, new_count: usize) -> *mut JsValue { + use std::alloc::Layout; + debug_assert!(new_count > 4); + debug_assert!(new_count <= 32, "overflow limited to 32 slots max"); + let n = new_count - 4; + let layout = Layout::array::(n).unwrap(); + let ptr: *mut JsValue; + unsafe { + ptr = _bump.alloc_layout(layout).as_ptr() as *mut JsValue; + for i in 0..n { + *ptr.add(i) = JsValue::undefined(); + } + } + self.overflow = ptr; + ptr + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn object_size_64_bytes() { + assert_eq!( + std::mem::size_of::(), + 64, + "JsObject layout mismatch — expected 64B (one cache line)" + ); + } + + #[test] + fn new_empty_defaults() { + let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert_eq!(obj.shape_id(), EMPTY_SHAPE_ID); + assert_eq!(obj.prop_count(), 0); + assert!(obj.is_extensible()); + assert!(!obj.is_array()); + assert!(!obj.is_function()); + assert_eq!(obj.generation(), 1); + } + + #[test] + fn shape_id_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_shape_id(0x00AB_CDEF); + assert_eq!(obj.shape_id(), 0x00AB_CDEF); + } + + #[test] + fn prop_count_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_prop_count(17); + assert_eq!(obj.prop_count(), 17); + } + + #[test] + fn flags_individual() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert!(obj.is_extensible()); + obj.set_extensible(false); + assert!(!obj.is_extensible()); + } + + #[test] + fn inline_prop_read_write() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + obj.set_inline_prop(0, JsValue::int(42)); + assert_eq!(obj.get_inline_prop(0), JsValue::int(42)); + } + + #[test] + fn new_array_flags() { + let obj = JsObject::new_array(5, JsValue::null(), 3); + assert!(obj.is_array()); + assert_eq!(obj.shape_id(), 5); + } + + #[test] + fn generation_bump() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert_eq!(obj.generation(), 1); + obj.bump_generation(); + assert_eq!(obj.generation(), 2); + } +}