Break up library into macros and map nested libraries for shared dependency with constant map macro.

This commit is contained in:
paladin 2026-10-02 10:02:13 +01:00
parent 805874a4c1
commit d037e22164
8 changed files with 88 additions and 39 deletions

337
map/src/lib.rs Normal file
View file

@ -0,0 +1,337 @@
// 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::fmt::Debug;
use std::hash::{DefaultHasher, Hash, Hasher};
type Displacement = u8;
#[cfg(feature = "bighash")]
pub type Hashed = u64;
#[cfg(not(feature = "bighash"))]
pub type Hashed = u32;
pub trait Equivalent<T>: Sized {
fn matches(&self, other: &T) -> bool;
}
impl<T: PartialEq> Equivalent<T> for T {
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
}
}
pub trait TableEntry: Equivalent<Self> + Hashes {
const VACANT: Self;
fn is_vacant(&self) -> bool;
}
#[derive(Debug)]
pub struct Entry<E: TableEntry> {
pub entry: E,
home: usize,
displacement: Displacement,
}
impl<E: TableEntry> Entry<E> {
const VACANT: Entry<E> = Entry {
entry: E::VACANT,
home: 0,
displacement: 0,
};
}
#[derive(Debug)]
pub struct DisplacementSet<E: TableEntry> {
// default representing vacancy!
pub map: Vec<Entry<E>>, // todo: make this private and use an iterator
map_bounds: std::ops::Range<usize>,
map_count: usize, // number of elements in table
}
pub fn get<E: TableEntry + Equivalent<K> ,K: Hashes>(map: &[Entry<E>], index: K) -> Option<&E> {
if map.len() == 0 {
return None;
}
let location = index.hashed() as usize % map.len();
let home = &map[location];
if home.entry.matches(&index) {
Some(&home.entry)
} else {
for i in 1..home.displacement as usize {
let neighbour = &map[(location + i) % map.len()];
if neighbour.entry.matches(&index) {
return Some(&neighbour.entry);
}
}
None
}
}
impl<E: TableEntry> DisplacementSet<E> {
fn exchange(&mut self, len: usize) {
let old = std::mem::replace(
&mut self.map,
(0..len).map(|_| Entry::VACANT).collect(), // todo: I don't like that this may go over all memory
);
self.map_bounds = self.lower()..self.upper();
for Entry { entry, .. } in old {
if !entry.is_vacant() {
self.set(entry)
}
}
}
pub fn resize(&mut self, len: usize) {
self.exchange(len.max(self.map_count + (self.map_count / 3)));
self.map_bounds = 0..self.upper();
}
fn upper(&self) -> usize {
(self.map.len() / 4) * 3
}
fn lower(&self) -> usize {
self.map.len() / 3
}
fn ensure(&mut self) {
if self.map_count + 1 > self.upper() {
// free space is short
self.exchange((self.map.len() + 4) * 2);
} else if self.map_count < self.lower() {
// too much free space
self.exchange(self.map.len() / 2)
}
}
pub fn set(&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();
}
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<usize> = 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((self.map.len() + 4) * 2);
self.set(entry);
}
}
pub fn remove<K>(&mut self, index: K)
where
E: Equivalent<K>,
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::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::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::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<K>(&mut self, index: K) -> Option<&E>
where
E: Equivalent<K>,
K: Hashes,
{
get(self.map.as_slice(),index)
}
pub fn new() -> DisplacementSet<E> {
DisplacementSet {
map: Vec::new(),
map_count: 0,
map_bounds: (0..0).into(),
}
}
}
#[derive(Clone)]
pub struct MapEntry<K: Hashes + PartialEq, V>(pub Option<(K, V)>);
impl<K: PartialEq + Hashes, V> Equivalent<K> for MapEntry<K, V> {
fn matches(&self, other: &K) -> bool {
&self.0.as_ref().unwrap().0 == other
}
}
impl<K: Hashes + PartialEq, V> Hashes for MapEntry<K, V> {
fn hashed(&self) -> Hashed {
self.0.as_ref().unwrap().0.hashed()
}
}
impl<K: Hashes + PartialEq, V> PartialEq for MapEntry<K, V> {
fn eq(&self, other: &Self) -> bool {
self.0.as_ref().unwrap().0 == other.0.as_ref().unwrap().0
}
}
impl<K: Hashes + PartialEq, V> TableEntry for MapEntry<K, V> {
const VACANT: Self = MapEntry(None);
fn is_vacant(&self) -> bool {
self.0.is_none()
}
}
pub struct Map<K: Hashes + PartialEq, V>(pub DisplacementSet<MapEntry<K, V>>);
impl<K: Hashes + PartialEq, V> Map<K, V> {
pub fn resize(&mut self, len: usize) {
self.0.resize(len)
}
pub fn set(&mut self, index: K, value: V) {
self.0.set(MapEntry(Some((index, value))));
}
pub fn remove(&mut self, index: K) {
self.0.remove(index);
}
pub fn get(&mut self, index: K) -> Option<&V> {
if let Some(ref entry) = self.0.get(index) {
Some(&entry.0.as_ref().unwrap().1)
} else {
None
}
}
pub fn new() -> Map<K, V> {
Map(DisplacementSet::new())
}
}
impl<K: Debug + Hashes + PartialEq, V: Debug> Debug for Map<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("Map")) // todo: debug and print the entire thing?
}
}