use crate::error::{LuaError, LuaResult, RuntimeError};
use super::gc::arena::{Arena, GcRef};
use super::gc::trace::Trace;
use super::string::LuaString;
use super::value::Val;
struct Node {
key: Val,
value: Val,
next: Option<u32>,
}
impl Node {
const fn empty() -> Self {
Self {
key: Val::Nil,
value: Val::Nil,
next: None,
}
}
}
fn hash_number(n: f64) -> u32 {
let n = n + 1.0;
let bits = n.to_bits();
let lo = bits as u32;
let hi = (bits >> 32) as u32;
lo.wrapping_add(hi)
}
#[inline]
fn hash_pow2(hash: u32, size: u32) -> u32 {
hash & size.wrapping_sub(1)
}
#[inline]
fn hash_mod(hash: u32, size: u32) -> u32 {
let divisor = size.wrapping_sub(1) | 1;
hash % divisor
}
fn number_to_int_key(n: f64) -> Option<i64> {
if !n.is_finite() {
return None;
}
let k = n as i64;
#[allow(clippy::float_cmp)]
if (k as f64) == n { Some(k) } else { None }
}
fn ceil_log2(n: usize) -> u8 {
if n <= 1 {
return 0;
}
(usize::BITS - (n - 1).leading_zeros()) as u8
}
const MAXBITS: u8 = 26;
const MAXASIZE: usize = 1 << MAXBITS;
fn runtime_error(msg: &str) -> LuaError {
LuaError::Runtime(RuntimeError {
message: msg.into(),
level: 0,
traceback: Vec::new(),
})
}
pub struct Table {
array: Vec<Val>,
nodes: Vec<Node>,
last_free: u32,
log2_size: u8,
metatable: Option<GcRef<Self>>,
flags: u8,
}
impl Table {
pub fn new() -> Self {
Self {
array: Vec::new(),
nodes: Vec::new(),
last_free: 0,
log2_size: 0,
metatable: None,
flags: 0,
}
}
pub fn with_sizes(array_size: usize, hash_size: usize) -> Self {
let array = vec![Val::Nil; array_size];
if hash_size == 0 {
Self {
array,
nodes: Vec::new(),
last_free: 0,
log2_size: 0,
metatable: None,
flags: 0,
}
} else {
let log2 = ceil_log2(hash_size);
let actual_size = 1u32 << log2;
let mut nodes = Vec::with_capacity(actual_size as usize);
for _ in 0..actual_size {
nodes.push(Node::empty());
}
Self {
array,
nodes,
last_free: actual_size,
log2_size: log2,
metatable: None,
flags: 0,
}
}
}
#[inline]
pub fn array_len(&self) -> usize {
self.array.len()
}
pub fn estimated_memory(&self) -> usize {
self.array.len() * 16 + self.hash_size() as usize * 32
}
#[inline]
pub fn hash_size(&self) -> u32 {
if self.log2_size == 0 && self.nodes.is_empty() {
0
} else {
1u32 << self.log2_size
}
}
#[inline]
pub fn metatable(&self) -> Option<GcRef<Self>> {
self.metatable
}
#[inline]
pub fn set_metatable(&mut self, mt: Option<GcRef<Self>>) {
self.metatable = mt;
self.flags = 0; }
#[inline]
pub fn array_slice(&self) -> &[Val] {
&self.array
}
pub fn hash_entries(&self) -> Vec<(Val, Val)> {
let mut entries = Vec::new();
for node in &self.nodes {
if !node.key.is_nil() {
entries.push((node.key, node.value));
}
}
entries
}
pub(crate) fn last_free_index(&self) -> u32 {
self.last_free
}
pub(crate) fn query_node(&self, idx: u32) -> Option<(Val, Val, Option<u32>)> {
let node = self.nodes.get(idx as usize)?;
Some((node.key, node.value, node.next))
}
pub(crate) fn array_get(&self, idx: usize) -> Option<Val> {
self.array.get(idx).copied()
}
pub(crate) fn ensure_array_capacity(&mut self, min_size: usize) {
if min_size > self.array.len() {
self.array.resize(min_size, Val::Nil);
}
}
#[inline]
pub fn hash_node_count(&self) -> u32 {
self.nodes.len() as u32
}
#[inline]
pub fn hash_node_kv(&self, idx: u32) -> Option<(Val, Val)> {
let node = self.nodes.get(idx as usize)?;
if node.key.is_nil() {
None
} else {
Some((node.key, node.value))
}
}
pub fn find_dead_hash_entries<F>(
&self,
weak_keys: bool,
weak_values: bool,
is_dead: F,
) -> Vec<usize>
where
F: Fn(Val, bool) -> bool,
{
let mut dead = Vec::new();
for (i, node) in self.nodes.iter().enumerate() {
if node.key.is_nil() {
continue;
}
if (weak_keys && is_dead(node.key, true)) || (weak_values && is_dead(node.value, false))
{
dead.push(i);
}
}
dead
}
pub fn nil_array_entry(&mut self, idx: usize) {
if idx < self.array.len() {
self.array[idx] = Val::Nil;
}
}
pub fn nil_hash_entries(&mut self, indices: &[usize]) {
for &idx in indices {
if idx < self.nodes.len() {
self.nodes[idx].value = Val::Nil;
}
}
}
#[inline]
pub fn flags(&self) -> u8 {
self.flags
}
#[inline]
pub fn set_tm_flag(&mut self, event: u8) {
if event <= 4 {
self.flags |= 1u8 << event;
}
}
#[inline]
pub fn invalidate_tm_cache(&mut self) {
self.flags = 0;
}
fn count_int(key: &Val, nums: &mut [u32; MAXBITS as usize + 1]) -> u32 {
if let Val::Num(n) = key
&& let Some(k) = number_to_int_key(*n)
&& k >= 1
&& (k as usize) <= MAXASIZE
{
nums[ceil_log2(k as usize) as usize] += 1;
return 1;
}
0
}
fn num_use_array(&self, nums: &mut [u32; MAXBITS as usize + 1]) -> u32 {
let mut ause: u32 = 0;
let mut i: usize = 1; for lg in 0..=MAXBITS {
let ttlg = 1usize << lg;
let lim = ttlg.min(self.array.len());
if i > lim {
break;
}
let mut lc: u32 = 0;
while i <= lim {
if !self.array[i - 1].is_nil() {
lc += 1;
}
i += 1;
}
nums[lg as usize] += lc;
ause += lc;
}
ause
}
fn num_use_hash(&self, nums: &mut [u32; MAXBITS as usize + 1], na_size: &mut u32) -> u32 {
let mut total_use: u32 = 0;
for node in self.nodes.iter().rev() {
if !node.value.is_nil() {
*na_size += Self::count_int(&node.key, nums);
total_use += 1;
}
}
total_use
}
fn compute_sizes(nums: &[u32; MAXBITS as usize + 1], na_size: &mut u32) -> u32 {
let mut a: u32 = 0; let mut na: u32 = 0; let mut best_size: u32 = 0; let mut twotoi: u32 = 1;
for num in nums {
if *num > 0 {
a += *num;
if a > twotoi / 2 {
best_size = twotoi;
na = a;
}
}
if a == *na_size {
break; }
twotoi = twotoi.saturating_mul(2);
}
*na_size = best_size;
na
}
fn rehash(&mut self, ek: &Val, strings: &Arena<LuaString>) -> LuaResult<()> {
let mut nums = [0u32; MAXBITS as usize + 1];
let mut na_size = self.num_use_array(&mut nums);
let mut total_use = na_size;
total_use += self.num_use_hash(&mut nums, &mut na_size);
na_size += Self::count_int(ek, &mut nums);
total_use += 1;
let na = Self::compute_sizes(&nums, &mut na_size);
let nh_size = total_use - na;
self.resize(na_size as usize, nh_size as usize, strings)
}
fn resize(
&mut self,
new_array_size: usize,
new_hash_size: usize,
strings: &Arena<LuaString>,
) -> LuaResult<()> {
let old_array_size = self.array.len();
if new_array_size > old_array_size {
self.array.resize(new_array_size, Val::Nil);
}
let old_nodes = if new_hash_size == 0 {
let old = core::mem::take(&mut self.nodes);
self.log2_size = 0;
self.last_free = 0;
old
} else {
let log2 = ceil_log2(new_hash_size);
if log2 > MAXBITS {
return Err(runtime_error("table overflow"));
}
let actual_size = 1u32 << log2;
let mut new_nodes = Vec::with_capacity(actual_size as usize);
for _ in 0..actual_size {
new_nodes.push(Node::empty());
}
let old = core::mem::replace(&mut self.nodes, new_nodes);
self.log2_size = log2;
self.last_free = actual_size;
old
};
if new_array_size < old_array_size {
for i in new_array_size..old_array_size {
if !self.array[i].is_nil() {
let key = Val::Num((i + 1) as f64);
let val = self.array[i];
let node_idx = self.new_key(key, strings)?;
self.nodes[node_idx as usize].value = val;
}
}
self.array.truncate(new_array_size);
}
for node in old_nodes.into_iter().rev() {
if !node.value.is_nil() {
self.raw_set_impl(node.key, node.value, strings, false)?;
}
}
Ok(())
}
pub(crate) fn main_position(&self, key: &Val, strings: &Arena<LuaString>) -> u32 {
let size = self.hash_size();
debug_assert!(size > 0, "main_position called on empty hash");
match key {
Val::Num(n) => hash_mod(hash_number(*n), size),
Val::Str(r) => {
let h = strings.get(*r).map_or(0, LuaString::hash);
hash_pow2(h, size)
}
Val::Bool(b) => hash_pow2(u32::from(*b), size),
Val::LightUserdata(p) => hash_mod(*p as u32, size),
Val::Table(r) => hash_mod(r.index(), size),
Val::Function(r) => hash_mod(r.index(), size),
Val::Userdata(r) => hash_mod(r.index(), size),
Val::Thread(r) => hash_mod(r.index(), size),
Val::Nil => 0,
}
}
fn get_free_pos(&mut self) -> Option<u32> {
while self.last_free > 0 {
self.last_free -= 1;
if self.nodes[self.last_free as usize].key.is_nil() {
return Some(self.last_free);
}
}
None
}
pub fn get_int(&self, key: i64) -> Val {
let idx = (key as u64).wrapping_sub(1);
if idx < self.array.len() as u64 {
return self.array[idx as usize];
}
if self.nodes.is_empty() {
return Val::Nil;
}
let nk = key as f64;
let mp = hash_mod(hash_number(nk), self.hash_size());
let mut cur = Some(mp);
while let Some(i) = cur {
let node = &self.nodes[i as usize];
#[allow(clippy::float_cmp)]
if let Val::Num(n) = node.key
&& n == nk
{
return node.value;
}
cur = node.next;
}
Val::Nil
}
pub fn get_str(&self, key: GcRef<LuaString>, strings: &Arena<LuaString>) -> Val {
if self.nodes.is_empty() {
return Val::Nil;
}
let Some(s) = strings.get(key) else {
return Val::Nil;
};
let mp = hash_pow2(s.hash(), self.hash_size());
let mut cur = Some(mp);
while let Some(i) = cur {
let node = &self.nodes[i as usize];
if node.key == Val::Str(key) {
return node.value;
}
cur = node.next;
}
Val::Nil
}
pub fn get(&self, key: Val, strings: &Arena<LuaString>) -> Val {
match key {
Val::Nil => Val::Nil,
Val::Str(r) => self.get_str(r, strings),
Val::Num(n) => {
if let Some(k) = number_to_int_key(n) {
self.get_int(k)
} else {
self.get_hash(key, strings)
}
}
_ => self.get_hash(key, strings),
}
}
fn get_hash(&self, key: Val, strings: &Arena<LuaString>) -> Val {
if self.nodes.is_empty() {
return Val::Nil;
}
let mp = self.main_position(&key, strings);
let mut cur = Some(mp);
while let Some(i) = cur {
let node = &self.nodes[i as usize];
if node.key == key {
return node.value;
}
cur = node.next;
}
Val::Nil
}
pub fn len(&self, strings: &Arena<LuaString>) -> usize {
let j = self.array.len();
if j > 0 && self.array[j - 1].is_nil() {
let mut lo: usize = 0;
let mut hi: usize = j;
while hi - lo > 1 {
let m = usize::midpoint(lo, hi);
if self.array[m - 1].is_nil() {
hi = m;
} else {
lo = m;
}
}
return lo;
}
if self.nodes.is_empty() {
return j;
}
self.unbound_search(j, strings)
}
fn unbound_search(&self, array_size: usize, strings: &Arena<LuaString>) -> usize {
let mut i = array_size; let mut j = array_size + 1;
while self.get(Val::Num(j as f64), strings) != Val::Nil {
i = j;
j = match j.checked_mul(2) {
Some(doubled) => doubled,
None => {
return self.linear_boundary_search(strings);
}
};
}
while j - i > 1 {
let m = usize::midpoint(i, j);
if self.get(Val::Num(m as f64), strings) == Val::Nil {
j = m;
} else {
i = m;
}
}
i
}
fn linear_boundary_search(&self, strings: &Arena<LuaString>) -> usize {
let mut i: usize = 1;
while self.get(Val::Num(i as f64), strings) != Val::Nil {
i += 1;
}
i - 1
}
pub fn next(&self, key: Val, strings: &Arena<LuaString>) -> LuaResult<Option<(Val, Val)>> {
let idx = self.find_index(key, strings)?;
let start = idx.wrapping_add(1);
if start < self.array.len() {
for i in start..self.array.len() {
if !self.array[i].is_nil() {
return Ok(Some((Val::Num((i + 1) as f64), self.array[i])));
}
}
}
let hash_start = if start >= self.array.len() {
start.wrapping_sub(self.array.len())
} else {
0
};
for i in hash_start..self.nodes.len() {
let node = &self.nodes[i];
if !node.value.is_nil() {
return Ok(Some((node.key, node.value)));
}
}
Ok(None) }
fn find_index(&self, key: Val, strings: &Arena<LuaString>) -> LuaResult<usize> {
if key.is_nil() {
return Ok(usize::MAX);
}
if let Val::Num(n) = key
&& let Some(k) = number_to_int_key(n)
{
let idx = (k as u64).wrapping_sub(1);
if idx < self.array.len() as u64 {
return Ok(idx as usize);
}
}
if !self.nodes.is_empty() {
let mp = self.main_position(&key, strings);
let mut cur = Some(mp);
while let Some(i) = cur {
if self.nodes[i as usize].key == key {
return Ok(i as usize + self.array.len());
}
cur = self.nodes[i as usize].next;
}
}
Err(runtime_error("invalid key to 'next'"))
}
pub fn raw_set(&mut self, key: Val, value: Val, strings: &Arena<LuaString>) -> LuaResult<()> {
if let Val::Str(r) = key
&& let Some(s) = strings.get(r)
&& s.data().starts_with(b"__")
{
self.flags = 0;
}
self.raw_set_impl(key, value, strings, true)
}
fn raw_set_impl(
&mut self,
key: Val,
value: Val,
strings: &Arena<LuaString>,
allow_rehash: bool,
) -> LuaResult<()> {
match key {
Val::Nil => return Err(runtime_error("table index is nil")),
Val::Num(n) if n.is_nan() => return Err(runtime_error("table index is NaN")),
_ => {}
}
if let Val::Num(n) = key
&& let Some(k) = number_to_int_key(n)
{
let idx = (k as u64).wrapping_sub(1);
if idx < self.array.len() as u64 {
self.array[idx as usize] = value;
return Ok(());
}
}
if !self.nodes.is_empty() {
let mp = self.main_position(&key, strings);
let mut cur = Some(mp);
while let Some(i) = cur {
if self.nodes[i as usize].key == key {
self.nodes[i as usize].value = value;
return Ok(());
}
cur = self.nodes[i as usize].next;
}
}
match self.new_key(key, strings) {
Ok(node_idx) => {
self.nodes[node_idx as usize].value = value;
Ok(())
}
Err(_) if allow_rehash => {
self.rehash(&key, strings)?;
self.raw_set_impl(key, value, strings, false)
}
Err(e) => Err(e),
}
}
fn new_key(&mut self, key: Val, strings: &Arena<LuaString>) -> LuaResult<u32> {
if self.nodes.is_empty() {
return Err(runtime_error("table overflow"));
}
let mp = self.main_position(&key, strings);
if self.nodes[mp as usize].key.is_nil() {
self.nodes[mp as usize].key = key;
return Ok(mp);
}
if self.nodes[mp as usize].value.is_nil()
&& let Val::Str(r) = self.nodes[mp as usize].key
&& strings.get(r).is_none()
{
self.nodes[mp as usize].key = key;
return Ok(mp);
}
let Some(free) = self.get_free_pos() else {
return Err(runtime_error("table overflow"));
};
let occupant_key = self.nodes[mp as usize].key;
let other_mp = self.main_position(&occupant_key, strings);
if other_mp == mp {
self.nodes[free as usize].next = self.nodes[mp as usize].next;
self.nodes[mp as usize].next = Some(free);
self.nodes[free as usize].key = key;
Ok(free)
} else {
let mut prev = other_mp;
while self.nodes[prev as usize].next != Some(mp) {
if let Some(next) = self.nodes[prev as usize].next {
prev = next;
} else {
break;
}
}
self.nodes[prev as usize].next = Some(free);
let occ_key = self.nodes[mp as usize].key;
let occ_val = self.nodes[mp as usize].value;
let occ_next = self.nodes[mp as usize].next;
self.nodes[free as usize].key = occ_key;
self.nodes[free as usize].value = occ_val;
self.nodes[free as usize].next = occ_next;
self.nodes[mp as usize] = Node::empty();
self.nodes[mp as usize].key = key;
Ok(mp)
}
}
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
impl Trace for Table {
fn trace(&self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vm::gc::Color;
use crate::vm::gc::arena::Arena;
use crate::vm::string::StringTable;
fn intern_str(
arena: &mut Arena<LuaString>,
table: &mut StringTable,
s: &[u8],
) -> GcRef<LuaString> {
table.intern(s, arena, Color::White0)
}
#[test]
fn ceil_log2_values() {
assert_eq!(ceil_log2(0), 0);
assert_eq!(ceil_log2(1), 0);
assert_eq!(ceil_log2(2), 1);
assert_eq!(ceil_log2(3), 2);
assert_eq!(ceil_log2(4), 2);
assert_eq!(ceil_log2(5), 3);
assert_eq!(ceil_log2(8), 3);
assert_eq!(ceil_log2(9), 4);
assert_eq!(ceil_log2(32), 5);
assert_eq!(ceil_log2(33), 6);
}
#[test]
fn number_to_int_key_integers() {
assert_eq!(number_to_int_key(1.0), Some(1));
assert_eq!(number_to_int_key(0.0), Some(0));
assert_eq!(number_to_int_key(-1.0), Some(-1));
assert_eq!(number_to_int_key(100.0), Some(100));
}
#[test]
fn number_to_int_key_non_integers() {
assert_eq!(number_to_int_key(1.5), None);
assert_eq!(number_to_int_key(0.1), None);
assert_eq!(number_to_int_key(f64::NAN), None);
assert_eq!(number_to_int_key(f64::INFINITY), None);
}
#[test]
fn number_to_int_key_negative_zero() {
assert_eq!(number_to_int_key(-0.0), Some(0));
}
#[test]
fn hash_number_negative_zero_equals_positive_zero() {
assert_eq!(hash_number(-0.0), hash_number(0.0));
}
#[test]
fn empty_table() {
let t = Table::new();
assert_eq!(t.array_len(), 0);
assert_eq!(t.hash_size(), 0);
assert!(t.metatable().is_none());
}
#[test]
fn table_with_sizes() {
let t = Table::with_sizes(10, 8);
assert_eq!(t.array_len(), 10);
assert_eq!(t.hash_size(), 8);
}
#[test]
fn table_hash_rounds_up_to_power_of_2() {
let t = Table::with_sizes(0, 5);
assert_eq!(t.hash_size(), 8); }
#[test]
fn table_default() {
let t = Table::default();
assert_eq!(t.array_len(), 0);
assert_eq!(t.hash_size(), 0);
}
#[test]
fn get_set_array_integers() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(10, 0);
t.raw_set(Val::Num(1.0), Val::Num(100.0), &strings_arena)
.ok();
t.raw_set(Val::Num(5.0), Val::Num(500.0), &strings_arena)
.ok();
t.raw_set(Val::Num(10.0), Val::Num(1000.0), &strings_arena)
.ok();
assert_eq!(t.get_int(1), Val::Num(100.0));
assert_eq!(t.get_int(5), Val::Num(500.0));
assert_eq!(t.get_int(10), Val::Num(1000.0));
assert_eq!(t.get_int(2), Val::Nil);
assert_eq!(t.get_int(0), Val::Nil);
assert_eq!(t.get_int(-1), Val::Nil);
}
#[test]
fn integer_float_equivalence() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(10, 0);
t.raw_set(Val::Num(3.0), Val::Num(42.0), &strings_arena)
.ok();
assert_eq!(t.get_int(3), Val::Num(42.0));
assert_eq!(t.get(Val::Num(3.0), &strings_arena), Val::Num(42.0));
}
#[test]
fn overwrite_array_value() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 0);
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
assert_eq!(t.get_int(1), Val::Num(10.0));
t.raw_set(Val::Num(1.0), Val::Num(20.0), &strings_arena)
.ok();
assert_eq!(t.get_int(1), Val::Num(20.0));
}
#[test]
fn set_nil_removes_array_value() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 0);
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
t.raw_set(Val::Num(1.0), Val::Nil, &strings_arena).ok();
assert_eq!(t.get_int(1), Val::Nil);
}
#[test]
fn get_set_string_keys() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let key_a = intern_str(&mut strings_arena, &mut str_table, b"hello");
let key_b = intern_str(&mut strings_arena, &mut str_table, b"world");
let mut t = Table::with_sizes(0, 4);
t.raw_set(Val::Str(key_a), Val::Num(1.0), &strings_arena)
.ok();
t.raw_set(Val::Str(key_b), Val::Num(2.0), &strings_arena)
.ok();
assert_eq!(t.get_str(key_a, &strings_arena), Val::Num(1.0));
assert_eq!(t.get_str(key_b, &strings_arena), Val::Num(2.0));
}
#[test]
fn get_set_boolean_keys() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 4);
t.raw_set(Val::Bool(true), Val::Num(1.0), &strings_arena)
.ok();
t.raw_set(Val::Bool(false), Val::Num(0.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Bool(true), &strings_arena), Val::Num(1.0));
assert_eq!(t.get(Val::Bool(false), &strings_arena), Val::Num(0.0));
}
#[test]
fn get_nonexistent_key_returns_nil() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let key = intern_str(&mut strings_arena, &mut str_table, b"missing");
let t = Table::with_sizes(5, 4);
assert_eq!(t.get_int(1), Val::Nil);
assert_eq!(t.get_str(key, &strings_arena), Val::Nil);
assert_eq!(t.get(Val::Num(99.0), &strings_arena), Val::Nil);
}
#[test]
fn get_from_empty_table() {
let strings_arena = Arena::new();
let t = Table::new();
assert_eq!(t.get_int(1), Val::Nil);
assert_eq!(t.get(Val::Num(1.0), &strings_arena), Val::Nil);
assert_eq!(t.get(Val::Bool(true), &strings_arena), Val::Nil);
}
#[test]
fn integer_out_of_array_goes_to_hash() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 4);
t.raw_set(Val::Num(10.0), Val::Num(100.0), &strings_arena)
.ok();
assert_eq!(t.get_int(10), Val::Num(100.0));
assert_eq!(t.get(Val::Num(10.0), &strings_arena), Val::Num(100.0));
}
#[test]
fn zero_key_goes_to_hash() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 4);
t.raw_set(Val::Num(0.0), Val::Bool(true), &strings_arena)
.ok();
assert_eq!(t.get_int(0), Val::Bool(true));
}
#[test]
fn negative_key_goes_to_hash() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 4);
t.raw_set(Val::Num(-1.0), Val::Bool(true), &strings_arena)
.ok();
assert_eq!(t.get_int(-1), Val::Bool(true));
}
#[test]
fn float_key_not_integer_in_hash() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 4);
t.raw_set(Val::Num(1.5), Val::Num(99.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(1.5), &strings_arena), Val::Num(99.0));
assert_eq!(t.get_int(1), Val::Nil);
}
#[test]
fn nil_key_is_error() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 4);
let result = t.raw_set(Val::Nil, Val::Num(1.0), &strings_arena);
let Err(err) = result else {
unreachable!("nil key should produce an error");
};
assert_eq!(err.to_string(), "table index is nil");
}
#[test]
fn nan_key_is_error() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 4);
let result = t.raw_set(Val::Num(f64::NAN), Val::Num(1.0), &strings_arena);
let Err(err) = result else {
unreachable!("NaN key should produce an error");
};
assert_eq!(err.to_string(), "table index is NaN");
}
#[test]
fn nil_get_returns_nil() {
let strings_arena = Arena::new();
let t = Table::with_sizes(5, 4);
assert_eq!(t.get(Val::Nil, &strings_arena), Val::Nil);
}
#[test]
fn set_nil_to_nonexistent_is_noop() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 4);
t.raw_set(Val::Num(42.0), Val::Nil, &strings_arena).ok();
assert_eq!(t.get(Val::Num(42.0), &strings_arena), Val::Nil);
}
#[test]
fn hash_collision_resolved() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 2);
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
t.raw_set(Val::Num(2.0), Val::Num(20.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(1.0), &strings_arena), Val::Num(10.0));
assert_eq!(t.get(Val::Num(2.0), &strings_arena), Val::Num(20.0));
}
#[test]
fn multiple_keys_in_hash() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let mut t = Table::with_sizes(0, 8);
let key_s = intern_str(&mut strings_arena, &mut str_table, b"name");
t.raw_set(Val::Str(key_s), Val::Num(1.0), &strings_arena)
.ok();
t.raw_set(Val::Num(42.0), Val::Num(2.0), &strings_arena)
.ok();
t.raw_set(Val::Bool(true), Val::Num(3.0), &strings_arena)
.ok();
t.raw_set(Val::Bool(false), Val::Num(4.0), &strings_arena)
.ok();
assert_eq!(t.get_str(key_s, &strings_arena), Val::Num(1.0));
assert_eq!(t.get(Val::Num(42.0), &strings_arena), Val::Num(2.0));
assert_eq!(t.get(Val::Bool(true), &strings_arena), Val::Num(3.0));
assert_eq!(t.get(Val::Bool(false), &strings_arena), Val::Num(4.0));
}
#[test]
fn fill_hash_part_completely() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 4);
for i in 0..4 {
let key = Val::Num(f64::from(i));
t.raw_set(key, Val::Num(f64::from(i * 10)), &strings_arena)
.ok();
}
for i in 0..4 {
let key = Val::Num(f64::from(i));
assert_eq!(t.get(key, &strings_arena), Val::Num(f64::from(i * 10)));
}
}
#[test]
fn overflow_triggers_rehash() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 2);
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
t.raw_set(Val::Num(2.0), Val::Num(20.0), &strings_arena)
.ok();
t.raw_set(Val::Num(3.0), Val::Num(30.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(1.0), &strings_arena), Val::Num(10.0));
assert_eq!(t.get(Val::Num(2.0), &strings_arena), Val::Num(20.0));
assert_eq!(t.get(Val::Num(3.0), &strings_arena), Val::Num(30.0));
}
#[test]
fn set_on_empty_hash_triggers_rehash() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 0);
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
let mut str_arena = Arena::new();
let mut str_tbl = StringTable::new();
let key = intern_str(&mut str_arena, &mut str_tbl, b"x");
t.raw_set(Val::Str(key), Val::Num(1.0), &str_arena).ok();
assert_eq!(t.get_str(key, &str_arena), Val::Num(1.0));
assert!(t.hash_size() > 0);
}
#[test]
fn metatable_get_set() {
let mut table_arena: Arena<Table> = Arena::new();
let mt_ref = table_arena.alloc(Table::new(), Color::White0);
let mut t = Table::new();
assert!(t.metatable().is_none());
t.set_metatable(Some(mt_ref));
assert_eq!(t.metatable(), Some(mt_ref));
t.set_metatable(None);
assert!(t.metatable().is_none());
}
#[test]
fn mixed_array_and_hash() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let mut t = Table::with_sizes(3, 4);
let key_name = intern_str(&mut strings_arena, &mut str_table, b"name");
t.raw_set(Val::Num(1.0), Val::Num(100.0), &strings_arena)
.ok();
t.raw_set(Val::Num(2.0), Val::Num(200.0), &strings_arena)
.ok();
t.raw_set(Val::Str(key_name), Val::Num(999.0), &strings_arena)
.ok();
t.raw_set(Val::Num(10.0), Val::Num(1000.0), &strings_arena)
.ok();
assert_eq!(t.get_int(1), Val::Num(100.0));
assert_eq!(t.get_int(2), Val::Num(200.0));
assert_eq!(t.get_str(key_name, &strings_arena), Val::Num(999.0));
assert_eq!(t.get_int(10), Val::Num(1000.0));
}
#[test]
fn negative_zero_and_positive_zero_same_key() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 4);
t.raw_set(Val::Num(0.0), Val::Num(42.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(-0.0), &strings_arena), Val::Num(42.0));
t.raw_set(Val::Num(-0.0), Val::Num(99.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(0.0), &strings_arena), Val::Num(99.0));
}
#[test]
fn rehash_empty_table_grows() {
let strings_arena = Arena::new();
let mut t = Table::new();
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(1.0), &strings_arena), Val::Num(10.0));
}
#[test]
fn rehash_moves_integers_to_array() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(0, 4);
for i in 1..=4 {
t.raw_set(
Val::Num(f64::from(i)),
Val::Num(f64::from(i * 10)),
&strings_arena,
)
.ok();
}
t.raw_set(Val::Num(5.0), Val::Num(50.0), &strings_arena)
.ok();
for i in 1..=5 {
assert_eq!(
t.get(Val::Num(f64::from(i)), &strings_arena),
Val::Num(f64::from(i * 10))
);
}
assert!(t.array_len() > 0);
}
#[test]
fn rehash_mixed_keys_splits_correctly() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let mut t = Table::new();
for i in 1..=8 {
t.raw_set(
Val::Num(f64::from(i)),
Val::Num(f64::from(i)),
&strings_arena,
)
.ok();
}
let key_a = intern_str(&mut strings_arena, &mut str_table, b"a");
let key_b = intern_str(&mut strings_arena, &mut str_table, b"b");
t.raw_set(Val::Str(key_a), Val::Bool(true), &strings_arena)
.ok();
t.raw_set(Val::Str(key_b), Val::Bool(false), &strings_arena)
.ok();
for i in 1..=8 {
assert_eq!(
t.get(Val::Num(f64::from(i)), &strings_arena),
Val::Num(f64::from(i))
);
}
assert_eq!(t.get_str(key_a, &strings_arena), Val::Bool(true));
assert_eq!(t.get_str(key_b, &strings_arena), Val::Bool(false));
assert!(t.array_len() >= 8);
}
#[test]
fn rehash_sparse_keys_stay_in_hash() {
let strings_arena = Arena::new();
let mut t = Table::new();
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
t.raw_set(Val::Num(1000.0), Val::Num(20.0), &strings_arena)
.ok();
assert_eq!(t.get(Val::Num(1.0), &strings_arena), Val::Num(10.0));
assert_eq!(t.get(Val::Num(1000.0), &strings_arena), Val::Num(20.0));
assert!(t.array_len() < 100);
}
#[test]
fn rehash_preserves_string_keys() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let mut t = Table::with_sizes(0, 2);
let key_x = intern_str(&mut strings_arena, &mut str_table, b"x");
let key_y = intern_str(&mut strings_arena, &mut str_table, b"y");
t.raw_set(Val::Str(key_x), Val::Num(1.0), &strings_arena)
.ok();
t.raw_set(Val::Str(key_y), Val::Num(2.0), &strings_arena)
.ok();
let key_z = intern_str(&mut strings_arena, &mut str_table, b"z");
t.raw_set(Val::Str(key_z), Val::Num(3.0), &strings_arena)
.ok();
assert_eq!(t.get_str(key_x, &strings_arena), Val::Num(1.0));
assert_eq!(t.get_str(key_y, &strings_arena), Val::Num(2.0));
assert_eq!(t.get_str(key_z, &strings_arena), Val::Num(3.0));
}
#[test]
fn many_inserts_grow_correctly() {
let strings_arena = Arena::new();
let mut t = Table::new();
for i in 1..=100 {
t.raw_set(
Val::Num(f64::from(i)),
Val::Num(f64::from(i * 3)),
&strings_arena,
)
.ok();
}
for i in 1..=100 {
assert_eq!(
t.get(Val::Num(f64::from(i)), &strings_arena),
Val::Num(f64::from(i * 3))
);
}
}
#[test]
fn compute_sizes_fifty_percent_threshold() {
let mut nums = [0u32; MAXBITS as usize + 1];
nums[0] = 1; nums[1] = 1; nums[2] = 1; let mut na_size: u32 = 3;
let na = Table::compute_sizes(&nums, &mut na_size);
assert_eq!(na_size, 4); assert_eq!(na, 3);
let mut nums2 = [0u32; MAXBITS as usize + 1];
nums2[0] = 1; nums2[ceil_log2(1_000_000) as usize] = 1; let mut na_size2: u32 = 2;
let na2 = Table::compute_sizes(&nums2, &mut na_size2);
assert!(na_size2 <= 2); assert!(na2 <= 2);
}
#[test]
fn len_empty_table() {
let strings_arena = Arena::new();
let t = Table::new();
assert_eq!(t.len(&strings_arena), 0);
}
#[test]
fn len_contiguous_array() {
let strings_arena = Arena::new();
let mut t = Table::new();
for i in 1..=5 {
t.raw_set(
Val::Num(f64::from(i)),
Val::Num(f64::from(i)),
&strings_arena,
)
.ok();
}
assert_eq!(t.len(&strings_arena), 5);
}
#[test]
fn len_array_with_nil_at_end() {
let strings_arena = Arena::new();
let mut t = Table::with_sizes(5, 0);
t.raw_set(Val::Num(1.0), Val::Bool(true), &strings_arena)
.ok();
t.raw_set(Val::Num(2.0), Val::Bool(true), &strings_arena)
.ok();
t.raw_set(Val::Num(3.0), Val::Bool(true), &strings_arena)
.ok();
assert_eq!(t.len(&strings_arena), 3);
}
#[test]
fn len_single_element() {
let strings_arena = Arena::new();
let mut t = Table::new();
t.raw_set(Val::Num(1.0), Val::Bool(true), &strings_arena)
.ok();
assert_eq!(t.len(&strings_arena), 1);
}
#[test]
fn len_only_hash_keys() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let mut t = Table::new();
let key = intern_str(&mut strings_arena, &mut str_table, b"hello");
t.raw_set(Val::Str(key), Val::Bool(true), &strings_arena)
.ok();
assert_eq!(t.len(&strings_arena), 0);
}
#[test]
fn len_pre_allocated_array() {
let strings_arena = Arena::new();
let t = Table::with_sizes(10, 0);
assert_eq!(t.len(&strings_arena), 0);
}
#[test]
fn next_empty_table() {
let strings_arena = Arena::new();
let t = Table::new();
let result = t.next(Val::Nil, &strings_arena);
let Ok(None) = result else {
unreachable!("empty table should return None");
};
}
#[test]
fn next_array_traversal() {
let strings_arena = Arena::new();
let mut t = Table::new();
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
t.raw_set(Val::Num(2.0), Val::Num(20.0), &strings_arena)
.ok();
t.raw_set(Val::Num(3.0), Val::Num(30.0), &strings_arena)
.ok();
let Ok(Some((k1, v1))) = t.next(Val::Nil, &strings_arena) else {
unreachable!("should have first entry");
};
assert_eq!(k1, Val::Num(1.0));
assert_eq!(v1, Val::Num(10.0));
let Ok(Some((k2, v2))) = t.next(k1, &strings_arena) else {
unreachable!("should have second entry");
};
assert_eq!(k2, Val::Num(2.0));
assert_eq!(v2, Val::Num(20.0));
let Ok(Some((k3, v3))) = t.next(k2, &strings_arena) else {
unreachable!("should have third entry");
};
assert_eq!(k3, Val::Num(3.0));
assert_eq!(v3, Val::Num(30.0));
let Ok(None) = t.next(k3, &strings_arena) else {
unreachable!("should be end of iteration");
};
}
#[test]
fn next_collects_all_entries() {
let mut strings_arena = Arena::new();
let mut str_table = StringTable::new();
let mut t = Table::new();
t.raw_set(Val::Num(1.0), Val::Num(100.0), &strings_arena)
.ok();
t.raw_set(Val::Num(2.0), Val::Num(200.0), &strings_arena)
.ok();
let key_a = intern_str(&mut strings_arena, &mut str_table, b"a");
t.raw_set(Val::Str(key_a), Val::Bool(true), &strings_arena)
.ok();
let mut entries = Vec::new();
let mut key = Val::Nil;
loop {
let Ok(result) = t.next(key, &strings_arena) else {
unreachable!("next should not error");
};
let Some((k, v)) = result else {
break;
};
entries.push((k, v));
key = k;
}
assert_eq!(entries.len(), 3);
assert_eq!(entries[0], (Val::Num(1.0), Val::Num(100.0)));
assert_eq!(entries[1], (Val::Num(2.0), Val::Num(200.0)));
assert_eq!(entries[2], (Val::Str(key_a), Val::Bool(true)));
}
#[test]
fn next_invalid_key_errors() {
let strings_arena = Arena::new();
let mut t = Table::new();
t.raw_set(Val::Num(1.0), Val::Num(10.0), &strings_arena)
.ok();
let result = t.next(Val::Num(999.0), &strings_arena);
assert!(result.is_err());
}
#[test]
fn table_trace_is_stub() {
let t = Table::new();
t.trace(); assert!(t.needs_trace()); }
#[test]
fn rehash_preserves_live_entries_after_nil_clearing() {
let mut arena: Arena<LuaString> = Arena::new();
let mut strtable = StringTable::new();
let mut table = Table::new();
let mut keys = Vec::new();
for i in 0..60 {
let name = format!("global_{i}");
let r = intern_str(&mut arena, &mut strtable, name.as_bytes());
table.raw_set(Val::Str(r), Val::Bool(true), &arena).ok();
keys.push((name, r));
}
let assert_ref = intern_str(&mut arena, &mut strtable, b"assert");
table
.raw_set(Val::Str(assert_ref), Val::Bool(true), &arena)
.ok();
for (_, r) in &keys {
table.raw_set(Val::Str(*r), Val::Nil, &arena).ok();
}
let new_key = intern_str(&mut arena, &mut strtable, b"t");
table
.raw_set(Val::Str(new_key), Val::Bool(true), &arena)
.ok();
let val = table.get_str(assert_ref, &arena);
assert!(!val.is_nil(), "assert lost after rehash");
}
#[test]
fn new_key_reuses_dead_occupant_slot() {
let mut arena: Arena<LuaString> = Arena::new();
let mut strtable = StringTable::new();
let mut table = Table::new();
let assert_ref = intern_str(&mut arena, &mut strtable, b"assert");
table
.raw_set(Val::Str(assert_ref), Val::Bool(true), &arena)
.ok();
let mut live_refs = Vec::new();
for i in 0..10 {
let name = format!("live_{i}");
let r = intern_str(&mut arena, &mut strtable, name.as_bytes());
table
.raw_set(Val::Str(r), Val::Num(f64::from(i)), &arena)
.ok();
live_refs.push((name, r));
}
let mut dead_refs = Vec::new();
for i in 0..50 {
let name = format!("dead_{i}");
let r = intern_str(&mut arena, &mut strtable, name.as_bytes());
table.raw_set(Val::Str(r), Val::Bool(true), &arena).ok();
dead_refs.push(r);
}
for &r in &dead_refs {
table.raw_set(Val::Str(r), Val::Nil, &arena).ok();
}
for &r in &dead_refs {
arena.free(r);
}
strtable.sweep_dead(&arena);
for i in 0..5 {
let name = format!("new_{i}");
let r = intern_str(&mut arena, &mut strtable, name.as_bytes());
table.raw_set(Val::Str(r), Val::Bool(true), &arena).ok();
}
let val = table.get_str(assert_ref, &arena);
assert!(!val.is_nil(), "assert lost after new_key with stale keys");
for (name, r) in &live_refs {
let val = table.get_str(*r, &arena);
assert!(!val.is_nil(), "live entry '{name}' lost");
}
}
}