341 lines
13 KiB
Rust
341 lines
13 KiB
Rust
use crate::gc::{Gc, Traverse};
|
|
use crate::{Callable, RunError, Value};
|
|
use std::rc::Rc;
|
|
|
|
// I've already spent 2 months on this interpreter, 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.
|
|
*/
|
|
|
|
type Displacement = u8;
|
|
pub type Hashed = usize;
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Entry {
|
|
index: Value,
|
|
item: Value,
|
|
home: usize,
|
|
displacement: Displacement,
|
|
}
|
|
|
|
impl Default for Entry {
|
|
fn default() -> Self {
|
|
Entry {
|
|
index: Value::Nil,
|
|
item: Value::Nil,
|
|
home: 0,
|
|
displacement: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn hash(value: &Value) -> Hashed {
|
|
match value {
|
|
Value::Nil => 0,
|
|
Value::Bool(boolean) => {
|
|
if *boolean {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
Value::String(_string, hash) => *hash,
|
|
Value::Function(callable) => match callable {
|
|
Callable::Rust(native) => Rc::as_ptr(native).addr(),
|
|
Callable::Mars(closure) => closure.addr(),
|
|
},
|
|
Value::Integer(integer) => *integer as usize,
|
|
Value::Number(number) => {
|
|
if number.is_nan() {
|
|
0
|
|
} else {
|
|
number.to_bits() as usize
|
|
}
|
|
}
|
|
Value::Table(table) => table.addr(),
|
|
Value::Object(object) => object.addr(),
|
|
#[cfg(feature = "vector3")]
|
|
Value::Vector(vec) => {
|
|
let mut hasher = DefaultHasher::new();
|
|
vec.as_u64vec3().hash(&mut hasher);
|
|
hasher.finish() as usize
|
|
}
|
|
}
|
|
}
|
|
|
|
// todo: displaced items don't ever get shuffled closer to their homes unless the table is resized
|
|
#[derive(Debug, Clone)]
|
|
pub struct Table {
|
|
table: Vec<Entry>,
|
|
array: Vec<Value>,
|
|
table_bounds: std::ops::Range<usize>,
|
|
table_count: usize, // number of elements in table
|
|
pub meta: Option<Gc<Table>>,
|
|
}
|
|
|
|
impl Traverse for Table {
|
|
fn traverse(&self) {
|
|
for Entry { index, item, .. } in self.table.iter() {
|
|
if !matches!(index, Value::Nil) {
|
|
index.traverse();
|
|
item.traverse();
|
|
}
|
|
}
|
|
for item in self.array.iter() {
|
|
item.traverse()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Table {
|
|
fn exchange_table(&mut self, len: usize) {
|
|
let old = std::mem::replace(&mut self.table, vec![Entry::default(); len]);
|
|
for Entry { index, item, .. } in old {
|
|
if index != Value::Nil {
|
|
self.set_table(index, item)
|
|
}
|
|
}
|
|
}
|
|
pub fn resize_table(&mut self, len: usize) {
|
|
self.exchange_table(len.max(self.table_count + (self.table_count / 3)));
|
|
self.table_bounds = 0..self.table_upper();
|
|
}
|
|
pub fn resize_array(&mut self, len: usize) {
|
|
self.array.resize(len, Value::Nil)
|
|
}
|
|
fn table_upper(&self) -> usize {
|
|
(self.table.len() / 4) * 3
|
|
}
|
|
fn table_lower(&self) -> usize {
|
|
self.table.len() / 3
|
|
}
|
|
fn ensure_table(&mut self) {
|
|
if self.table_count > self.table_upper() {
|
|
// free space is short
|
|
self.exchange_table((self.table.len() + 4) * 2);
|
|
} else if self.table_count < self.table_lower() {
|
|
// too much free space
|
|
self.exchange_table(self.table.len() / 2)
|
|
}
|
|
self.table_bounds = self.table_lower()..self.table_upper()
|
|
}
|
|
fn set_table(&mut self, index: Value, item: Value) {
|
|
#[cfg(feature = "assertions")]
|
|
assert_ne!(index, Value::Nil);
|
|
#[cfg(feature = "assertions")]
|
|
assert_ne!(item, Value::Nil);
|
|
if !self.table_bounds.contains(&self.table_count) {
|
|
// make sure the table is appropriately sized
|
|
self.ensure_table();
|
|
}
|
|
let range = self.table.len();
|
|
let home = hash(&index) % range;
|
|
if self.table[home].index == Value::Nil {
|
|
// attempt to place it directly in an empty space
|
|
// INSERT
|
|
self.table[home].home = home;
|
|
self.table[home].item = item; // home.item.soft_drop()
|
|
self.table[home].index = index;
|
|
self.table[home].displacement = self.table[home].displacement.max(0);
|
|
self.table_count += 1;
|
|
} else if self.table[home].index == index {
|
|
// attempt to replace it directly
|
|
// REPLACE
|
|
self.table[home].item = item; // home.item.soft_drop()
|
|
} else {
|
|
// attempt to replace it in a collided neighbour location
|
|
for i in 1..self.table[home].displacement + 1 {
|
|
let neighbour = &mut self.table[(home + i as usize) % range]; // todo: is mod expensive?
|
|
if neighbour.index == index {
|
|
// found where it was displaced to
|
|
// REPLACE
|
|
neighbour.item = item;
|
|
return;
|
|
}
|
|
}
|
|
// at this point it must be added, probe to place in an empty space
|
|
for j in self.table[home].displacement + 1..Displacement::MAX - 1 {
|
|
let neighbour = &mut self.table[(home + j as usize) % range];
|
|
if neighbour.index == Value::Nil {
|
|
// new empty slot hooray!
|
|
// INSERT
|
|
neighbour.home = home;
|
|
neighbour.item = item;
|
|
neighbour.index = index;
|
|
self.table[home].displacement = j;
|
|
self.table_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.table.len() {
|
|
let neighbour = &mut self.table[k];
|
|
if neighbour.index == Value::Nil && free.is_none() {
|
|
free = Some(k);
|
|
} else if neighbour.index == index {
|
|
// REPLACE
|
|
neighbour.item = item;
|
|
return;
|
|
}
|
|
}
|
|
if let Some(k) = free {
|
|
// INSERT
|
|
self.table[k].home = home;
|
|
self.table[k].index = index;
|
|
self.table[k].item = item;
|
|
self.table[home].displacement = Displacement::MAX;
|
|
self.table_count += 1;
|
|
return;
|
|
}
|
|
// must resize
|
|
#[cfg(feature = "messages")]
|
|
eprintln!("\n\tResizing...");
|
|
self.resize_table((self.table.len() + 4) * 2);
|
|
self.set_table(index, item);
|
|
}
|
|
}
|
|
fn rem_table(&mut self, index: Value) {
|
|
let range = self.table.len();
|
|
let home = hash(&index) % range;
|
|
if self.table[home].index == index {
|
|
self.table[home].index = Value::Nil;
|
|
self.table[home].item = Value::Nil; // is this necessary?
|
|
self.table_count -= 1;
|
|
} else {
|
|
if self.table[home].displacement != Displacement::MAX {
|
|
let mut largest = 0;
|
|
for i in 1..self.table[home].displacement {
|
|
let neighbour = &mut self.table[(home + i as usize) % range];
|
|
if neighbour.index == index {
|
|
neighbour.index = Value::Nil;
|
|
neighbour.item = Value::Nil;
|
|
self.table_count -= 1;
|
|
if i == self.table[home].displacement {
|
|
self.table[home].displacement = largest
|
|
}
|
|
return;
|
|
} else if neighbour.index != Value::Nil && neighbour.home == home {
|
|
largest = i;
|
|
}
|
|
}
|
|
} else {
|
|
let mut largest = 0;
|
|
let mut finished = false; // what
|
|
for k in 0..self.table.len() {
|
|
let neighbour = &mut self.table[k];
|
|
if neighbour.index == index {
|
|
neighbour.index = Value::Nil;
|
|
neighbour.item = Value::Nil;
|
|
self.table_count -= 1;
|
|
#[cfg(feature = "assertions")]
|
|
assert!(!finished);
|
|
finished = true;
|
|
}
|
|
if neighbour.home == home && neighbour.index != Value::Nil {
|
|
largest = largest.max(if k < home {
|
|
k + self.table.len() - home - 1 // ?
|
|
} else {
|
|
k - home
|
|
})
|
|
}
|
|
}
|
|
self.table[home].displacement = if largest > Displacement::MAX as usize {
|
|
Displacement::MAX
|
|
} else {
|
|
largest as Displacement
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub fn set(&mut self, index: Value, item: Value) -> Result<(), RunError> {
|
|
match index {
|
|
Value::Integer(index) => {
|
|
// This is an integer index, try the array first
|
|
match item {
|
|
Value::Nil => {
|
|
if (0..self.array.len() + 1).contains(&(index as usize)) {
|
|
if self.array.len() == index as usize {
|
|
self.array.pop();
|
|
} else {
|
|
self.array[index as usize + 1] = Value::Nil
|
|
}
|
|
}
|
|
self.rem_table(Value::Integer(index));
|
|
}
|
|
item => {
|
|
if (0..self.array.len() + 1).contains(&(index as usize)) {
|
|
if self.array.len() == index as usize {
|
|
self.array.push(item)
|
|
} else {
|
|
self.array[index as usize + 1] = item;
|
|
}
|
|
} else {
|
|
self.set_table(Value::Integer(index), item)
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
Value::Nil => Err(RunError(
|
|
"Attempt to set new index of tabel with key: nil".to_string(),
|
|
)),
|
|
index => Ok(self.set_table(index, item)),
|
|
}
|
|
}
|
|
pub fn get(&self, index: Value) -> Result<Value, RunError> {
|
|
match index {
|
|
Value::Integer(index) => Ok(self
|
|
.array
|
|
.get(index as usize)
|
|
.unwrap_or(&Value::Nil)
|
|
.clone()),
|
|
Value::Nil => Err(RunError("Attempt to index table with key: nil".to_string())),
|
|
index => {
|
|
let location = hash(&index) % self.table.len();
|
|
let home = &self.table[location];
|
|
if home.index == index {
|
|
Ok(home.item.clone())
|
|
} else {
|
|
for i in 1..home.displacement as usize {
|
|
let neighbour = &self.table[location + i];
|
|
if neighbour.index == index {
|
|
return Ok(neighbour.item.clone());
|
|
}
|
|
}
|
|
Ok(Value::Nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub fn append(&mut self, item: Value) {
|
|
self.array.push(item)
|
|
}
|
|
pub fn new() -> Self {
|
|
Table {
|
|
table: Vec::new(),
|
|
array: Vec::new(),
|
|
table_count: 0,
|
|
table_bounds: (0..0).into(),
|
|
meta: None,
|
|
}
|
|
}
|
|
}
|