diff --git a/crates/oxide_kernel/src/lib.rs b/crates/oxide_kernel/src/lib.rs index a35e2c20..21252791 100644 --- a/crates/oxide_kernel/src/lib.rs +++ b/crates/oxide_kernel/src/lib.rs @@ -1,3 +1,4 @@ #![doc = "OxideJS - Shared kernel (CodeForge, ShapeForge, IcForge, StringForge, BuiltinWorld)"] +pub mod shape_forge; pub mod string_forge; diff --git a/crates/oxide_kernel/src/shape_forge.rs b/crates/oxide_kernel/src/shape_forge.rs new file mode 100644 index 00000000..2a453b52 --- /dev/null +++ b/crates/oxide_kernel/src/shape_forge.rs @@ -0,0 +1,255 @@ +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, RwLock}; + +use dashmap::DashMap; + +pub type ShapeId = u32; +pub type StringIndex = u32; + +pub const EMPTY_SHAPE_ID: ShapeId = 1; +const EMPTY_SENTINEL: StringIndex = u32::MAX; + +#[derive(Debug, Clone)] +pub struct Shape { + pub id: ShapeId, + pub property_name: StringIndex, + pub property_offset: u8, + pub parent: Option, +} + +pub struct ShapeForge { + shapes: RwLock>>>, + transitions: DashMap, + next_id: AtomicU32, +} + +impl ShapeForge { + pub fn new() -> Self { + let forge = Self { + shapes: RwLock::new(Vec::with_capacity(256)), + transitions: DashMap::with_capacity(256), + next_id: AtomicU32::new(2), + }; + { + let mut shapes = forge.shapes.write().unwrap(); + let empty = Arc::new(Shape { + id: EMPTY_SHAPE_ID, + property_name: EMPTY_SENTINEL, + property_offset: 0, + parent: None, + }); + debug_assert_eq!(shapes.len(), 0); + shapes.push(Some(empty)); + } + forge + } + + pub fn pack_key(parent_id: ShapeId, prop_name: StringIndex) -> u64 { + ((parent_id as u64) << 32) | (prop_name as u64) + } + + fn compute_offset_inner(shape_id: ShapeId, shapes: &[Option>]) -> u8 { + let mut count = 0u8; + let mut cursor = Some(shape_id); + while let Some(id) = cursor { + match shapes.get((id - 1) as usize).and_then(|s| s.clone()) { + Some(s) => { + if s.property_name != EMPTY_SENTINEL { + count += 1; + } + cursor = s.parent; + } + None => break, + } + } + count.min(31) + } + + pub fn make_shape(&self, parent_id: ShapeId, prop_name: StringIndex) -> ShapeId { + let key = Self::pack_key(parent_id, prop_name); + + if let Some(entry) = self.transitions.get(&key) { + return *entry; + } + + let shapes_read = self.shapes.read().unwrap(); + let prop_offset = Self::compute_offset_inner(parent_id, &shapes_read); + drop(shapes_read); + + let new_id = self.next_id.fetch_add(1, Ordering::Relaxed); + + let shape = Arc::new(Shape { + id: new_id, + property_name: prop_name, + property_offset: prop_offset, + parent: Some(parent_id), + }); + + { + let mut shapes = self.shapes.write().unwrap(); + while shapes.len() < new_id as usize { + shapes.push(None); + } + shapes[(new_id - 1) as usize] = Some(shape); + } + + *self.transitions.entry(key).or_insert(new_id).value() + } + + pub fn get_shape(&self, id: ShapeId) -> Option> { + let shapes = self.shapes.read().unwrap(); + shapes.get((id - 1) as usize).and_then(|s| s.clone()) + } + + pub fn lookup_offset(&self, shape_id: ShapeId, prop_name: StringIndex) -> Option { + let shapes = self.shapes.read().unwrap(); + let mut cursor = Some(shape_id); + while let Some(id) = cursor { + match shapes.get((id - 1) as usize).and_then(|s| s.clone()) { + Some(s) => { + if s.property_name == prop_name && s.property_name != EMPTY_SENTINEL { + return Some(s.property_offset); + } + cursor = s.parent; + } + None => return None, + } + } + None + } + + pub fn shape_prop_count(&self, shape_id: ShapeId) -> u8 { + let shapes = self.shapes.read().unwrap(); + let mut count = 0u8; + let mut cursor = Some(shape_id); + while let Some(id) = cursor { + match shapes.get((id - 1) as usize).and_then(|s| s.clone()) { + Some(s) => { + if s.property_name != EMPTY_SENTINEL { + count += 1; + } + cursor = s.parent; + } + None => break, + } + } + count + } +} + +impl Default for ShapeForge { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Barrier; + + #[test] + fn empty_shape_exists() { + let forge = ShapeForge::new(); + let s = forge.get_shape(EMPTY_SHAPE_ID); + assert!(s.is_some()); + let s = s.unwrap(); + assert_eq!(s.id, EMPTY_SHAPE_ID); + assert!(s.parent.is_none()); + } + + #[test] + fn make_shape_creates_new_id() { + let forge = ShapeForge::new(); + let key: StringIndex = 1_000_000; + let s1 = forge.make_shape(EMPTY_SHAPE_ID, key); + assert!(s1 > EMPTY_SHAPE_ID); + let shape = forge.get_shape(s1).unwrap(); + assert_eq!(shape.property_name, key); + assert_eq!(shape.parent, Some(EMPTY_SHAPE_ID)); + } + + #[test] + fn hash_cons_returns_same_id() { + let forge = ShapeForge::new(); + let key: StringIndex = 1_000_001; + let a = forge.make_shape(EMPTY_SHAPE_ID, key); + let b = forge.make_shape(EMPTY_SHAPE_ID, key); + assert_eq!(a, b); + } + + #[test] + fn different_props_different_ids() { + let forge = ShapeForge::new(); + let a = forge.make_shape(EMPTY_SHAPE_ID, 1_000_002); + let b = forge.make_shape(EMPTY_SHAPE_ID, 1_000_003); + assert_ne!(a, b); + } + + #[test] + fn chain_of_three() { + let forge = ShapeForge::new(); + let s1 = forge.make_shape(EMPTY_SHAPE_ID, 1_000_010); + let s2 = forge.make_shape(s1, 1_000_020); + let s3 = forge.make_shape(s2, 1_000_030); + + assert_eq!(forge.shape_prop_count(s3), 3); + + assert_eq!(forge.lookup_offset(s3, 1_000_030), Some(2)); + assert_eq!(forge.lookup_offset(s3, 1_000_020), Some(1)); + assert_eq!(forge.lookup_offset(s3, 1_000_010), Some(0)); + assert_eq!(forge.lookup_offset(s3, 99), None); + } + + #[test] + fn two_branches_share_ancestor() { + let forge = ShapeForge::new(); + let base = forge.make_shape(EMPTY_SHAPE_ID, 1_000_040); + let branch_a = forge.make_shape(base, 1_000_050); + let branch_b = forge.make_shape(base, 1_000_060); + assert_ne!(branch_a, branch_b); + let a_shape = forge.get_shape(branch_a).unwrap(); + let b_shape = forge.get_shape(branch_b).unwrap(); + assert_eq!(a_shape.parent, Some(base)); + assert_eq!(b_shape.parent, Some(base)); + } + + #[test] + fn edge_same_structure_different_names() { + let forge = ShapeForge::new(); + let s1 = forge.make_shape(EMPTY_SHAPE_ID, 1_000_070); + let s2 = forge.make_shape(s1, 1_000_080); + let s3 = forge.make_shape(EMPTY_SHAPE_ID, 1_000_090); + let s4 = forge.make_shape(s3, 1_000_100); + assert_ne!(s1, s3); + assert_ne!(s2, s4); + assert_eq!(forge.lookup_offset(s2, 1_000_070), Some(0)); + assert_eq!(forge.lookup_offset(s4, 1_000_090), Some(0)); + } + + #[test] + fn concurrent_make_same_key() { + let forge = Arc::new(ShapeForge::new()); + let key: StringIndex = 1_000_200; + let barrier = Arc::new(Barrier::new(2)); + + let f1 = Arc::clone(&forge); + let b1 = Arc::clone(&barrier); + let h1 = std::thread::spawn(move || { + b1.wait(); + f1.make_shape(EMPTY_SHAPE_ID, key) + }); + + let f2 = Arc::clone(&forge); + let b2 = Arc::clone(&barrier); + let h2 = std::thread::spawn(move || { + b2.wait(); + f2.make_shape(EMPTY_SHAPE_ID, key) + }); + + let id1 = h1.join().unwrap(); + let id2 = h2.join().unwrap(); + assert_eq!(id1, id2); + assert!(id1 > EMPTY_SHAPE_ID); + } +}