commit bae72484711baf8c794461d1e4b33ad363e1f52a Author: paladin Date: Mon Sep 28 02:01:19 2026 +0100 Implement freelists and displacement hashmaps. Garbage collector unfinished. Builds? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8cabc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6f3b4cd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "deimos" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..05d2d35 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "deimos" +version = "0.1.0" +edition = "2024" + +[features] +bighash = [] +assertions = [] +messages = [] \ No newline at end of file diff --git a/src/gc.rs b/src/gc.rs new file mode 100644 index 0000000..12e66a7 --- /dev/null +++ b/src/gc.rs @@ -0,0 +1,286 @@ +use std::hash::{Hash, Hasher}; +use std::marker::{PhantomData}; +use std::ops::{Deref, DerefMut}; + +type Index = u32; + +pub struct FreeList { + items: Vec>>, + free: Vec, + count: u32, +} + +#[derive(Clone,Debug)] +pub struct AnyId { + pub(crate) index: Index +} + +impl PartialEq for AnyId { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl From for Id { + fn from(value: AnyId) -> Self { + Id { + index: value.index, + phantom_data: PhantomData, + } + } +} + +#[derive(Debug)] +pub struct Id { + pub(crate) index: Index, + phantom_data: PhantomData, +} + +pub struct RefId<'a, T> { + pub id: Id, + it: &'a mut T +} + +impl<'a, T> Deref for RefId<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.it + } +} + +impl<'a, T> DerefMut for RefId<'a, T> { + + fn deref_mut(&mut self) -> &mut Self::Target { + self.it + } +} + +impl From> for Id { + fn from(value: RefId) -> Self { + value.id + } +} + +impl Clone for Id { + fn clone(&self) -> Self { + Id { + index: self.index, + phantom_data: PhantomData::default(), + } + } +} + +impl PartialEq for Id { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl Eq for Id {} + +impl Hash for Id { + fn hash(&self, state: &mut H) { + self.index.hash(state) + } +} + +impl Id { + pub fn maybe(self) -> MaybeId { + MaybeId(self) + } + pub fn any(self) -> AnyId { AnyId { + index: self.index + } } +} + +#[derive(Debug)] +pub struct MaybeId(Id); + +impl From> for MaybeId { + fn from(value: Id) -> Self { + MaybeId(value) + } +} + +impl Clone for MaybeId { + fn clone(&self) -> Self { + if self.0.index != u32::MAX { + MaybeId(self.0.clone()) + } else { + MaybeId::NULL + } + } +} + +impl PartialEq for MaybeId { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl MaybeId { + pub const NULL: MaybeId = MaybeId(Id { index: u32::MAX, phantom_data: PhantomData {}, }); + pub fn unwrap(self) -> Id { + if self.0.index == u32::MAX { + panic!() + } else { + self.0 + } + } + pub fn exists(&self) -> Option> { + if self.0.index == u32::MAX { + None + } else { + Some(self.0.clone()) + } + } +} + +impl Default for FreeList { + fn default() -> Self { + Self::new() + } +} + +impl FreeList { + pub fn new() -> FreeList { + FreeList { + items: Vec::new(), + free: Vec::new(), + count: 0, + } + } + pub fn get(&mut self, id: &Id) -> &mut T { + &mut self.items[id.index as usize].as_mut().unwrap().it + } + pub fn get_ref(&mut self, id: Id) -> RefId<'_, T> { + let it = self.get(&id); + RefId { id, it } + } + pub fn remove(&mut self, id: &Id) -> T { + self.free.push(id.index); + self.items[id.index as usize].take().unwrap().it + } + pub fn make(&mut self, value: T) -> RefId<'_, T> { // todo: shouldn't panic if allocation fails + if let Some(free) = self.free.pop() { + self.items[free as usize] = Some(Header::new(value)); + RefId { + id: Id { + index: free, + phantom_data: PhantomData, + }, + it: &mut self.items[free as usize].as_mut().unwrap().it, + } + } else { + self.items.push(Some(Header::new(value))); + let index = (self.items.len() - 1) as Index; + RefId { + id: Id { + index, + phantom_data: PhantomData, + }, + it: &mut self.items[index as usize].as_mut().unwrap().it, + } + } + } +} + +pub struct IdNode { + item: T, + next: MaybeId>, +} + +impl Traverse for IdNode where C: Allocates { + fn traverse(id: &Id>, context: &mut C) { + if let Some(ref next) = context.freelist().get(&id).next.exists() { + IdNode::traverse(next,context) + } + } +} + +#[derive(Clone)] +pub struct LinkedList { + first: MaybeId>, + count: Index, +} + +impl Default for LinkedList { + fn default() -> Self { + LinkedList { + first: MaybeId::NULL, + count: 0, + } + } +} + +impl LinkedList { + pub fn new() -> LinkedList { + LinkedList { + first: MaybeId::NULL, + count: 0, + } + } + pub fn add(&mut self, list: &mut FreeNodeList, it: T) { + let old = std::mem::replace(&mut self.first,list.make(IdNode { + item: it, + next: MaybeId::NULL, + }).id.maybe()); + list.get(&self.first.clone().unwrap()).next = old; // todo: this is painful + self.count += 1; + } + pub fn remove(&mut self, list: &mut FreeNodeList, criteria: F) -> T where T: PartialEq, F: Fn(&T) -> bool { + // Find some occurrence of 'item' in the list, remove and restitch the list, returning its id + let mut maybe_last = MaybeId::NULL; + let mut maybe_this = self.first.clone(); + while let Some(this) = maybe_this.exists() { + let node = list.remove(&this); + let next = node.next.clone(); + if criteria(&node.item) { + self.count -= 1; + if let Some(last) = maybe_last.exists() { + list.get(&last).next = node.next.clone(); + } else { + self.first = node.next.clone(); + } + return node.item; + } + maybe_last = maybe_this; + maybe_this = next; + } + panic!() + } +} + +pub type FreeNodeList = FreeList>; + +#[derive(Clone, Debug, Copy)] +enum State { + Cold, + Warm, +} + +#[derive(Debug, Clone)] +struct Header { + state: State, + it: T, +} + +impl Header where { + fn new(it: T) -> Header { + Header { + state: State::Cold, + it, + } + } +} + +pub trait Allocates where Self: Sized, T: Traverse { + fn collect(); + fn freelist<'a>(&'a mut self) -> &'a mut FreeList; +} + +pub trait Traverse: Sized { + fn traverse(id: &Id, context: &mut C); + fn collect(self); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..938c419 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod gc; +pub mod map; \ No newline at end of file diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..5aae0ab --- /dev/null +++ b/src/map.rs @@ -0,0 +1,297 @@ +// I've already spent 2 months on this interpreter (now GC!?), and I'm tired, so I've cut a few corners... +/* See some of Lua's notes on this table: https://www.lua.org/source/5.5/ltable.c.html +This table uses an 'open-addressed' hashmap https://en.wikipedia.org/wiki/Open_addressing +where collisions are handled by 'linear-probing' and something I call 'displacement'. + +Motivation: +Afaik, without having some mechanism that records or ensures known proximity between a displaced element +that collided with other(s) in the hashmap and it's original home (at element's hash's index), +the entire array may have to be checked just to see if an element is present. How can we fix this? + +Solution: +Lua uses Brent's method, which I couldn't find a concrete explanation of, here I'm just recording +the maximum displacement that will need to be linearly-probed from the home entry to be certain of +any 'home' element's presence in the greater array. + +Notes: +- If this value becomes large, the load factor is probably high and there would be a resize. +- Values would only need to be shuffled around closer to their 'home' during a resize. +- It is unlikely the displacement would large value without a resize amending that problem. + */ +use std::hash::{DefaultHasher, Hash, Hasher}; +use crate::gc::{AnyId, Id}; + +type Displacement = u8; + +#[cfg(feature = "bighash")] +pub type Hashed = u64; +#[cfg(not(feature = "bighash"))] +pub type Hashed = u32; + +pub trait Equivalent: Sized { + fn matches(&self, other: &T) -> bool; +} + +impl Equivalent for T +where + T: PartialEq, +{ + fn matches(&self, other: &T) -> bool { + self == other + } +} + +pub trait Hashes { + fn hashed(&self) -> Hashed; +} + +impl Hashes for &str { + fn hashed(&self) -> Hashed { + let mut hasher = DefaultHasher::new(); + self[..self.len().min(6)].hash(&mut hasher); + hasher.finish() as Hashed + } +} + +impl Hashes for Id { + fn hashed(&self) -> Hashed { + let mut hasher = DefaultHasher::new(); + self.index.hash(&mut hasher); + hasher.finish() as Hashed + } +} + +impl Hashes for AnyId { + fn hashed(&self) -> Hashed { + let mut hasher = DefaultHasher::new(); + self.index.hash(&mut hasher); + hasher.finish() as Hashed + } +} + +pub trait TableEntry: Equivalent + Hashes { + fn new_vacant() -> Self; + fn is_vacant(&self) -> bool; +} + +#[derive(Debug, Clone)] +pub struct Entry { + pub entry: E, + home: usize, + displacement: Displacement, +} + +#[derive(Clone, Debug)] +pub struct Map { + // default representing vacancy! + pub map: Vec>, // todo: make this private and use an iterator + map_bounds: std::ops::Range, + map_count: usize, // number of elements in table +} + +impl Map { + fn exchange_table(&mut self, len: usize) { + let old = std::mem::replace( + &mut self.map, + vec![ + Entry { + entry: E::new_vacant(), + home: 0, + displacement: 0, + }; + len + ], + ); + self.map_bounds = self.table_lower()..self.table_upper(); + for Entry { entry, .. } in old { + if !entry.is_vacant() { + self.set_table(entry) + } + } + } + pub fn resize_table(&mut self, len: usize) { + self.exchange_table(len.max(self.map_count + (self.map_count / 3))); + self.map_bounds = 0..self.table_upper(); + } + fn table_upper(&self) -> usize { + (self.map.len() / 4) * 3 + } + fn table_lower(&self) -> usize { + self.map.len() / 3 + } + fn ensure_table(&mut self) { + if self.map_count + 1 > self.table_upper() { + // free space is short + self.exchange_table((self.map.len() + 4) * 2); + } else if self.map_count < self.table_lower() { + // too much free space + self.exchange_table(self.map.len() / 2) + } + } + pub fn set_table(&mut self, entry: E) { + #[cfg(feature = "assertions")] + assert!(!entry.is_vacant()); + if !self.map_bounds.contains(&(self.map_count + 1)) { + // make sure the table is appropriately sized + self.ensure_table(); + } + let range = self.map.len(); + let home = entry.hashed() as usize % range; + if self.map[home].entry.is_vacant() { + // attempt to place it directly in an empty space + // INSERT + self.map[home].home = home; + self.map[home].entry = entry; + self.map[home].displacement = self.map[home].displacement.max(0); + self.map_count += 1; + } else if self.map[home].entry.matches(&entry) { + // attempt to replace it directly + // REPLACE + self.map[home].entry = entry; // home.item.soft_drop() + } else { + // attempt to replace it in a collided neighbour location + for i in 1..self.map[home].displacement + 1 { + let neighbour = &mut self.map[(home + i as usize) % range]; // todo: is mod expensive? + if neighbour.entry.matches(&entry) { + // found where it was displaced to + // REPLACE + neighbour.entry = entry; + return; + } + } + // at this point it must be added, probe to place in an empty space + for j in self.map[home].displacement + 1..Displacement::MAX - 1 { + let neighbour = &mut self.map[(home + j as usize) % range]; + if neighbour.entry.is_vacant() { + // new empty slot hooray! + // INSERT + neighbour.entry = entry; + neighbour.home = home; + self.map[home].displacement = j; + self.map_count += 1; + return; + } + } + // impossible but this still needs to be complete + #[cfg(feature = "messages")] + eprintln!("Large table collision, are the hashes ok?\n\tBrute-force probing..."); + let mut free: Option = None; + for k in 0..self.map.len() { + let neighbour = &mut self.map[k]; + if neighbour.entry.is_vacant() && free.is_none() { + free = Some(k); + } else if neighbour.entry.matches(&entry) { + // REPLACE + neighbour.entry = entry; + return; + } + } + if let Some(k) = free { + // INSERT + self.map[k].home = home; + self.map[k].entry = entry; + self.map[home].displacement = Displacement::MAX; + self.map_count += 1; + return; + } + // must resize + #[cfg(feature = "messages")] + eprintln!("\n\tResizing..."); + self.resize_table((self.map.len() + 4) * 2); + self.set_table(entry); + } + } + pub fn rem_table(&mut self, index: K) + where + E: Equivalent, + K: Hashes, + { + if self.map.len() == 0 { + return; + } + let range = self.map.len(); + let home = index.hashed() as usize % range; + if self.map[home].entry.matches(&index) { + self.map[home].entry = E::new_vacant(); + self.map_count -= 1; + } else { + if self.map[home].displacement != Displacement::MAX { + let mut largest = 0; + for i in 1..self.map[home].displacement { + let neighbour = &mut self.map[(home + i as usize) % range]; // todo: maybe mod slows this down + if neighbour.entry.matches(&index) { + neighbour.entry = E::new_vacant(); + self.map_count -= 1; + if i == self.map[home].displacement { + self.map[home].displacement = largest + } + return; + } else if !neighbour.entry.is_vacant() && neighbour.home == home { + largest = i; + } + } + } else { + let mut largest = 0; + #[cfg(feature = "assertions")] + let mut finished = false; // what + for k in 0..self.map.len() { + let neighbour = &mut self.map[k]; + if neighbour.entry.matches(&index) { + neighbour.entry = E::new_vacant(); + self.map_count -= 1; + #[cfg(feature = "assertions")] + { + assert!(!finished); + finished = true; + } + } + if neighbour.home == home && !neighbour.entry.is_vacant() { + largest = largest.max(if k < home { + k + self.map.len() - home - 1 // ? + } else { + k - home + }) + } + } + self.map[home].displacement = if largest > Displacement::MAX as usize { + Displacement::MAX + } else { + largest as Displacement + } + } + } + } + pub fn get_table(&mut self, index: K) -> Option<&E> + where + E: Equivalent, + K: Hashes, + { + if self.map.len() == 0 { + return None; + } + let location = index.hashed() as usize % self.map.len(); + let home = &self.map[location]; + if home.entry.matches(&index) { + Some(&home.entry) + } else { + for i in 1..home.displacement as usize { + let neighbour = &self.map[(location + i) % self.map.len()]; + if neighbour.entry.matches(&index) { + return Some(&neighbour.entry); + } + } + None + } + } + pub fn new() -> Map { + Map { + map: Vec::new(), + map_count: 0, + map_bounds: (0..0).into(), + } + } +} + +pub struct TableEntry + +pub type Table = Map<> \ No newline at end of file