Skip to main content

lua_types/
table.rs

1//! Lua table implementation (array + hash hybrid).
2//!
3//! Canonical port of `reference/lua-5.4.7/src/ltable.c`. Lives in
4//! `lua-types` because `LuaValue::Table(GcRef<LuaTable>)` is defined here
5//! and the table storage must be reachable without depending on
6//! `lua-vm`. The crate `lua_unsafe = "forbid"` lint is preserved.
7//!
8//! # Interior mutability
9//!
10//! `GcRef<T>` only yields `&T` on deref, so the mutable algorithms in
11//! C-Lua's `ltable.c` (which write through `Table *`) must operate
12//! through a `RefCell`. The split is:
13//!
14//! * `LuaTable` — outer handle. All public methods take `&self`.
15//! * `TableInner` — storage + algorithms. All mutating methods are
16//!   `&mut TableInner` and are reached via `inner.borrow_mut()`.
17//!
18//! The hash part uses Brent's variation of chained scatter tables.
19//! The key invariant: if an element is not in its *main position*
20//! (the slot its hash maps to), the colliding element *is* in its
21//! own main position.
22
23use std::cell::{Cell, RefCell};
24
25use crate::closure::LuaClosure;
26use crate::error::LuaError;
27use crate::gc::GcRef;
28use crate::string::LuaString;
29use crate::value::LuaValue;
30
31// ── Constants ─────────────────────────────────────────────────────────────────
32
33/// Largest `k` such that `2^k` fits in a signed `i32`.
34const MAXABITS: u32 = (std::mem::size_of::<i32>() as u32) * 8 - 1;
35
36/// Maximum size of the array part.
37pub const MAXASIZE: u32 = 1u32 << MAXABITS;
38
39/// Largest `k` such that `2^k` fits in a signed `i32` minus one (hash part).
40pub const MAXHBITS: u32 = MAXABITS - 1;
41
42/// Maximum size of the hash part (power-of-2 count of nodes).
43const MAXHSIZE: u32 = 1u32 << MAXHBITS;
44
45/// Minimum hash node count when lazily materializing a brand-new dummy table
46/// on first non-array key insertion.
47///
48/// In workloads that create many tiny record-like tables (`binarytrees`),
49/// a dummy→rehash path on every first insert adds avoidable overhead.
50const DUMMY_TABLE_INIT_HASH_NODES: u32 = 4;
51
52/// Bit 7 of `TableFlags`: when set, `alimit` is NOT the real array size.
53const BIT_RAS: u8 = 1 << 7;
54
55/// Soft cap on array growth in a single `raw_set` call.
56///
57/// Prevents pathological inserts of a far-away integer key (e.g.
58/// `t[1<<30] = 1`) from allocating gigabytes when an array slot would
59/// be the wrong choice; falls back to the hash part for keys above the
60/// cap. Matches C-Lua's behavioural bound, which is governed by
61/// `MAXASIZE` and the rehash density heuristic.
62pub const ARRAY_GROW_CAP: u32 = 1u32 << 20;
63
64/// Soft cap on total entries (array + hash) used to emulate C-Lua's
65/// `malloc`-NULL termination of unbounded `for i = 1, math.huge do a[i] = ... end`
66/// loops. C-Lua hits real malloc failure at multi-gigabyte allocations; we
67/// raise `LuaError::Memory` (which pcall catches as `"not enough memory"`)
68/// once the table exceeds this size. Sized well above any realistic test
69/// workload (`big.lua` uses ~264K entries) while bounded enough that
70/// `heavy.lua` terminates within the harness timeout.
71pub const TOTAL_GROW_CAP: usize = 1usize << 20;
72
73const WEAK_KEYS: u8 = 1 << 0;
74const WEAK_VALUES: u8 = 1 << 1;
75
76// ── TableFlags ─────────────────────────────────────────────────────────────────
77
78/// Bitfield for a [`LuaTable`]: lower bits record absent fast-access
79/// metamethods; bit 7 encodes whether `alimit` is the real array size.
80#[derive(Clone, Copy, Debug, Default)]
81pub struct TableFlags(pub u8);
82
83impl TableFlags {
84    /// `isrealasize(t)` — bit 7 clear means alimit IS the real array size.
85    #[inline]
86    pub fn is_real_asize(self) -> bool {
87        (self.0 & BIT_RAS) == 0
88    }
89
90    /// `setrealasize(t)` — clear bit 7 so alimit becomes the canonical size.
91    #[inline]
92    pub fn set_real_asize(&mut self) {
93        self.0 &= !BIT_RAS;
94    }
95
96    /// `setnorealasize(t)` — set bit 7 so alimit is only a hint.
97    #[inline]
98    pub fn set_no_real_asize(&mut self) {
99        self.0 |= BIT_RAS;
100    }
101
102    /// `invalidateTMcache(t)` — clear all fast-access metamethod bits.
103    #[inline]
104    pub fn invalidate_tm_cache(&mut self) {
105        const MASK_FLAGS: u8 = 0x7F;
106        self.0 &= !MASK_FLAGS;
107    }
108}
109
110// ── TableNode ──────────────────────────────────────────────────────────────────
111
112/// One node in a table's hash part.
113///
114/// signed offset into the same node vector.
115pub struct TableNode {
116    /// Value stored at this key.  C: `gval(n)`.
117    pub value: LuaValue,
118    /// Key stored in this node.  C: `n->u.key_val` + `n->u.key_tt`.
119    pub key: LuaValue,
120    /// Collision-chain offset (positive or negative; zero means end of chain).
121    pub next: i32,
122}
123
124impl TableNode {
125    fn empty() -> Self {
126        TableNode { value: LuaValue::Nil, key: LuaValue::Nil, next: 0 }
127    }
128
129    fn key_is_nil(&self) -> bool { matches!(self.key, LuaValue::Nil) }
130    fn key_is_int(&self) -> bool { matches!(self.key, LuaValue::Int(_)) }
131    fn key_int(&self) -> i64 {
132        if let LuaValue::Int(i) = self.key { i }
133        else { panic!("TableNode::key_int: key is not int") }
134    }
135    fn key_is_short_str(&self) -> bool {
136        if let LuaValue::Str(s) = &self.key { s.is_short() }
137        else { false }
138    }
139    fn key_string(&self) -> &GcRef<LuaString> {
140        if let LuaValue::Str(s) = &self.key { s }
141        else { panic!("TableNode::key_string: key is not a string") }
142    }
143    fn key_value(&self) -> LuaValue { self.key.clone() }
144    fn set_key(&mut self, k: &LuaValue) { self.key = k.clone(); }
145}
146
147// ── TableSlotRef ───────────────────────────────────────────────────────────────
148
149/// Internal slot reference returned by the "get" family of functions.
150///
151/// Replaces C's `const TValue *` pattern, which may point into either
152/// the array part, the hash part, or the static `absentkey` sentinel.
153#[derive(Debug, Clone, Copy)]
154pub enum TableSlotRef {
155    /// Key lives in the array part at this 0-based index.
156    Array(usize),
157    /// Key lives in the hash part at this 0-based node index.
158    Hash(usize),
159    /// Key is absent from the table (C: `&absentkey`).
160    Absent,
161}
162
163// ── ceil_log2 ─────────────────────────────────────────────────────────────────
164
165/// Computes `ceil(log2(x))`; returns the minimum `k` such that `2^k >= x`.
166fn ceil_log2(x: u32) -> i32 {
167    static LOG_2: [u8; 256] = [
168        0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
169        6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
170        7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
171        7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
172        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
173        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
174        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
175        8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
176    ];
177    let mut l: i32 = 0;
178    let mut x = x.wrapping_sub(1);
179    while x >= 256 { l += 8; x >>= 8; }
180    l + LOG_2[x as usize] as i32
181}
182
183// ── float hash (frexp-based) ──────────────────────────────────────────────────
184
185/// Hash a `f64` to an `i32` bucket index.
186///
187/// Uses `frexp` decomposition to produce a well-distributed integer hash.
188/// Handles inf/NaN by returning 0.
189///
190fn hash_float(n: f64) -> i32 {
191    if n.is_nan() || n.is_infinite() {
192        return 0;
193    }
194    let (mantissa, exp) = frexp(n);
195    let scaled = mantissa * -(i32::MIN as f64);
196    let ni = scaled as i64;
197    if ni as f64 != scaled {
198        return 0;
199    }
200    let u = (exp as u32).wrapping_add(ni as u32);
201    if u <= i32::MAX as u32 { u as i32 } else { !(u as i32) }
202}
203
204/// Decompose `x` into mantissa ∈ `[0.5, 1)` and integer exponent.
205fn frexp(x: f64) -> (f64, i32) {
206    if x == 0.0 || x.is_nan() || x.is_infinite() {
207        return (x, 0);
208    }
209    let bits = x.to_bits();
210    let exp_bits = ((bits >> 52) & 0x7FFu64) as i32;
211    if exp_bits == 0 {
212        let scaled = x * (2.0f64.powi(64));
213        let (m, e) = frexp(scaled);
214        return (m, e - 64);
215    }
216    let exp = exp_bits - 1022;
217    let mantissa_bits = (bits & !(0x7FFu64 << 52)) | (0x3FEu64 << 52);
218    (f64::from_bits(mantissa_bits), exp)
219}
220
221// ── TableInner ─────────────────────────────────────────────────────────────────
222
223/// Hybrid array + hash storage backing a [`LuaTable`].
224///
225/// All mutating algorithms live as `&mut TableInner` methods so they
226/// can be called from the outer `&self` API via `RefCell::borrow_mut`.
227pub struct TableInner {
228    pub flags: TableFlags,
229    pub lsizenode: u8,
230    pub alimit: u32,
231    pub array: Vec<LuaValue>,
232    pub node: Vec<TableNode>,
233    pub lastfree: Option<usize>,
234}
235
236impl TableInner {
237    fn new() -> Self {
238        TableInner {
239            flags: TableFlags(0x7F),
240            lsizenode: 0,
241            alimit: 0,
242            array: Vec::new(),
243            node: Vec::new(),
244            lastfree: None,
245        }
246    }
247
248    /// `isdummy(t)` — true when the table has no allocated hash part.
249    #[inline]
250    fn is_dummy(&self) -> bool { self.lastfree.is_none() }
251
252    /// `sizenode(t)` — nominal hash-part capacity (`1 << lsizenode`).
253    #[inline]
254    fn sizenode(&self) -> u32 { 1u32 << self.lsizenode }
255
256    /// `allocsizenode(t)` — 0 when dummy, else `1 << lsizenode`.
257    #[inline]
258    fn alloc_sizenode(&self) -> u32 {
259        if self.is_dummy() { 0 } else { self.sizenode() }
260    }
261
262    /// `isrealasize(t)` accessor.
263    #[inline]
264    fn is_real_asize(&self) -> bool { self.flags.is_real_asize() }
265
266    /// `ispow2(x)` — C treats 0 as a power of two.
267    #[inline]
268    fn is_pow2(x: u32) -> bool { x == 0 || x.is_power_of_two() }
269
270    /// Returns the real size of the array part. C: `luaH_realasize`.
271    fn real_asize(&self) -> u32 {
272        if self.limit_equals_asize() {
273            return self.alimit;
274        }
275        let mut size = self.alimit;
276        size |= size >> 1;
277        size |= size >> 2;
278        size |= size >> 4;
279        size |= size >> 8;
280        size |= size >> 16;
281        size = size.wrapping_add(1);
282        debug_assert!(
283            Self::is_pow2(size) && size / 2 < self.alimit && self.alimit < size
284        );
285        size
286    }
287
288    #[inline]
289    fn limit_equals_asize(&self) -> bool {
290        self.is_real_asize() || Self::is_pow2(self.alimit)
291    }
292
293    fn is_pow2_real_asize(&self) -> bool {
294        !self.is_real_asize() || Self::is_pow2(self.alimit)
295    }
296
297    fn set_limit_to_size(&mut self) -> u32 {
298        self.alimit = self.real_asize();
299        self.flags.set_real_asize();
300        self.alimit
301    }
302
303    // ── Hash helper functions ──────────────────────────────────────────────
304
305    fn hash_idx_for_int(&self, i: i64) -> usize {
306        let ui = i as u64;
307        let sn = self.sizenode() as usize;
308        let modulo = (sn - 1) | 1;
309        if ui <= i32::MAX as u64 {
310            (ui as usize) % modulo
311        } else {
312            (ui as usize) % modulo
313        }
314    }
315
316    #[inline]
317    fn hashpow2_idx(&self, h: u32) -> usize {
318        (h & (self.sizenode() - 1)) as usize
319    }
320
321    #[inline]
322    fn hashmod_idx(&self, h: usize) -> usize {
323        let sn = self.sizenode() as usize;
324        let modulo = (sn - 1) | 1;
325        h % modulo
326    }
327
328    fn main_position(&self, key: &LuaValue) -> usize {
329        match key {
330            LuaValue::Int(i) => self.hash_idx_for_int(*i),
331            LuaValue::Float(f) => {
332                let h = hash_float(*f);
333                self.hashmod_idx(h as usize)
334            }
335            LuaValue::Str(s) if s.is_short() => self.hashpow2_idx(s.hash()),
336            LuaValue::Str(s) => self.hashpow2_idx(s.hash()),
337            LuaValue::Bool(false) => self.hashpow2_idx(0),
338            LuaValue::Bool(true) => self.hashpow2_idx(1),
339            LuaValue::LightUserData(p) => {
340                let h = (*p as usize as u32) as usize;
341                self.hashmod_idx(h)
342            }
343            LuaValue::Function(LuaClosure::LightC(f)) => {
344                let h = (*f as u32) as usize;
345                self.hashmod_idx(h)
346            }
347            LuaValue::Table(t) => {
348                let h = (GcRef::identity(t) as u32) as usize;
349                self.hashmod_idx(h)
350            }
351            LuaValue::Function(LuaClosure::Lua(cl)) => {
352                let h = (GcRef::identity(cl) as u32) as usize;
353                self.hashmod_idx(h)
354            }
355            LuaValue::Function(LuaClosure::C(cl)) => {
356                let h = (GcRef::identity(cl) as u32) as usize;
357                self.hashmod_idx(h)
358            }
359            LuaValue::UserData(u) => {
360                let h = (GcRef::identity(u) as u32) as usize;
361                self.hashmod_idx(h)
362            }
363            LuaValue::Thread(th) => {
364                let h = (GcRef::identity(th) as u32) as usize;
365                self.hashmod_idx(h)
366            }
367            LuaValue::Nil => 0,
368        }
369    }
370
371    fn main_position_from_node(&self, nd: usize) -> usize {
372        let key = self.node[nd].key_value();
373        self.main_position(&key)
374    }
375
376    // ── Key equality ───────────────────────────────────────────────────────
377
378    fn equal_key(k1: &LuaValue, n2: &TableNode) -> bool {
379        let types_match = std::mem::discriminant(k1) == std::mem::discriminant(&n2.key);
380        if !types_match {
381            return false;
382        }
383        match &n2.key {
384            LuaValue::Nil => true,
385            LuaValue::Bool(b) => matches!(k1, LuaValue::Bool(b2) if b == b2),
386            LuaValue::Int(ni) => matches!(k1, LuaValue::Int(ki) if ki == ni),
387            LuaValue::Float(nf) => matches!(k1, LuaValue::Float(kf) if kf == nf),
388            LuaValue::LightUserData(np) => matches!(k1, LuaValue::LightUserData(kp) if kp == np),
389            LuaValue::Function(LuaClosure::LightC(nf)) => {
390                matches!(k1, LuaValue::Function(LuaClosure::LightC(kf)) if kf == nf)
391            }
392            LuaValue::Str(ns) if ns.is_long() => {
393                if let LuaValue::Str(ks) = k1 {
394                    ks.as_bytes() == ns.as_bytes()
395                } else { false }
396            }
397            _ => Self::gc_ptr_eq(k1, &n2.key),
398        }
399    }
400
401    fn gc_ptr_eq(a: &LuaValue, b: &LuaValue) -> bool {
402        match (a, b) {
403            (LuaValue::Str(sa), LuaValue::Str(sb)) => GcRef::ptr_eq(sa, sb),
404            (LuaValue::Table(ta), LuaValue::Table(tb)) => GcRef::ptr_eq(ta, tb),
405            (LuaValue::Function(LuaClosure::Lua(fa)), LuaValue::Function(LuaClosure::Lua(fb))) => {
406                GcRef::ptr_eq(fa, fb)
407            }
408            (LuaValue::Function(LuaClosure::C(fa)), LuaValue::Function(LuaClosure::C(fb))) => {
409                GcRef::ptr_eq(fa, fb)
410            }
411            (LuaValue::UserData(ua), LuaValue::UserData(ub)) => GcRef::ptr_eq(ua, ub),
412            (LuaValue::Thread(ta), LuaValue::Thread(tb)) => GcRef::ptr_eq(ta, tb),
413            _ => false,
414        }
415    }
416
417    // ── Generic hash-part lookup ───────────────────────────────────────────
418
419    fn get_generic_slot(&self, key: &LuaValue) -> TableSlotRef {
420        if self.is_dummy() { return TableSlotRef::Absent; }
421        let mut n = self.main_position(key);
422        loop {
423            if Self::equal_key(key, &self.node[n]) {
424                return TableSlotRef::Hash(n);
425            }
426            let nx = self.node[n].next;
427            if nx == 0 { return TableSlotRef::Absent; }
428            n = (n as isize + nx as isize) as usize;
429        }
430    }
431
432    // ── arrayindex / findindex ─────────────────────────────────────────────
433
434    fn array_index(k: i64) -> u32 {
435        let uk = k as u64;
436        if uk.wrapping_sub(1) < MAXASIZE as u64 { k as u32 } else { 0 }
437    }
438
439    /// Find the linear traversal position of `key`. Returns 0 for `Nil`
440    /// (first iteration). Errors with `"invalid key to 'next'"` when
441    /// the key is non-nil and not present in the table.
442    fn find_index(&self, key: &LuaValue, asize: u32) -> Result<u32, LuaError> {
443        if matches!(key, LuaValue::Nil) { return Ok(0); }
444        let i = if let LuaValue::Int(k) = key { Self::array_index(*k) } else { 0 };
445        if i.wrapping_sub(1) < asize { return Ok(i); }
446        let slot = self.get_generic_slot(key);
447        match slot {
448            TableSlotRef::Absent => {
449                Err(LuaError::runtime(format_args!("invalid key to 'next'")))
450            }
451            TableSlotRef::Hash(node_idx) => Ok((node_idx as u32 + 1) + asize),
452            TableSlotRef::Array(_) => unreachable!("getgeneric returned Array slot"),
453        }
454    }
455
456    /// Iteration step: given a key (`Nil` for first call), return the
457    /// next `(key, value)` pair in C-Lua's array-then-hash order.
458    fn next_pair(&self, key: &LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
459        let asize = self.real_asize();
460        let i = self.find_index(key, asize)?;
461        let mut i = i as usize;
462        while i < asize as usize {
463            if !matches!(self.array[i], LuaValue::Nil) {
464                return Ok(Some((LuaValue::Int((i + 1) as i64), self.array[i].clone())));
465            }
466            i += 1;
467        }
468        let mut hi = i.saturating_sub(asize as usize);
469        while hi < self.node.len() {
470            if !matches!(self.node[hi].value, LuaValue::Nil) {
471                return Ok(Some((self.node[hi].key_value(), self.node[hi].value.clone())));
472            }
473            hi += 1;
474        }
475        Ok(None)
476    }
477
478    // ── Rehash helpers ─────────────────────────────────────────────────────
479
480    fn compute_sizes(nums: &[u32], pna: &mut u32) -> u32 {
481        let mut twotoi: u32 = 1;
482        let mut a: u32 = 0;
483        let mut na: u32 = 0;
484        let mut optimal: u32 = 0;
485        for i in 0..nums.len() {
486            if twotoi == 0 || *pna <= twotoi / 2 { break; }
487            a += nums[i];
488            if a > twotoi / 2 {
489                optimal = twotoi;
490                na = a;
491            }
492            twotoi = twotoi.wrapping_mul(2);
493        }
494        debug_assert!(optimal == 0 || optimal / 2 < na && na <= optimal);
495        *pna = na;
496        optimal
497    }
498
499    fn count_int(key: i64, nums: &mut [u32]) -> bool {
500        let k = Self::array_index(key);
501        if k != 0 {
502            nums[ceil_log2(k) as usize] += 1;
503            true
504        } else { false }
505    }
506
507    fn num_use_array(&self, nums: &mut [u32]) -> u32 {
508        debug_assert!(self.is_real_asize(), "numusearray: alimit must be real size");
509        let asize = self.alimit as usize;
510        let mut ause: u32 = 0;
511        let mut i: usize = 1;
512        let mut ttlg: usize = 1;
513        for lg in 0..=(MAXABITS as usize) {
514            let mut lc: u32 = 0;
515            let lim = if ttlg > asize { asize } else { ttlg };
516            if i > lim { break; }
517            while i <= lim {
518                if !matches!(self.array[i - 1], LuaValue::Nil) { lc += 1; }
519                i += 1;
520            }
521            nums[lg] += lc;
522            ause += lc;
523            ttlg = ttlg.saturating_mul(2);
524        }
525        ause
526    }
527
528    fn num_use_hash(&self, nums: &mut [u32], pna: &mut u32) -> i32 {
529        let mut totaluse: i32 = 0;
530        let mut ause: u32 = 0;
531        let mut i = self.node.len();
532        while i > 0 {
533            i -= 1;
534            let n = &self.node[i];
535            if !matches!(n.value, LuaValue::Nil) {
536                if n.key_is_int() {
537                    if Self::count_int(n.key_int(), nums) { ause += 1; }
538                }
539                totaluse += 1;
540            }
541        }
542        *pna += ause;
543        totaluse
544    }
545
546    fn set_node_vector(&mut self, size: u32) -> Result<(), LuaError> {
547        if size == 0 {
548            self.node = Vec::new();
549            self.lsizenode = 0;
550            self.lastfree = None;
551        } else {
552            let lsize = ceil_log2(size);
553            if lsize as u32 > MAXHBITS || (1u32 << lsize) > MAXHSIZE {
554                return Err(LuaError::runtime(format_args!("table overflow")));
555            }
556            let actual_size = 1u32 << lsize;
557            let mut nodes = Vec::with_capacity(actual_size as usize);
558            for _ in 0..actual_size { nodes.push(TableNode::empty()); }
559            self.node = nodes;
560            self.lsizenode = lsize as u8;
561            self.lastfree = Some(actual_size as usize);
562        }
563        Ok(())
564    }
565
566    fn reinsert(&mut self, old_nodes: Vec<(LuaValue, LuaValue)>) -> Result<(), LuaError> {
567        for (k, v) in old_nodes {
568            self.set(&k, v)?;
569        }
570        Ok(())
571    }
572
573    /// Resize the table to new array and hash sizes.
574    fn resize(&mut self, new_asize: u32, nhsize: u32) -> Result<(), LuaError> {
575        let old_asize = self.set_limit_to_size();
576
577        let (mut new_hash_node, mut new_hash_lsize, mut new_hash_lastfree) = {
578            let mut tmp = TableInner::new();
579            tmp.set_node_vector(nhsize)?;
580            (tmp.node, tmp.lsizenode, tmp.lastfree)
581        };
582
583        if new_asize < old_asize {
584            self.alimit = new_asize;
585            std::mem::swap(&mut self.node, &mut new_hash_node);
586            std::mem::swap(&mut self.lsizenode, &mut new_hash_lsize);
587            std::mem::swap(&mut self.lastfree, &mut new_hash_lastfree);
588
589            for i in (new_asize as usize)..(old_asize as usize) {
590                if !matches!(self.array[i], LuaValue::Nil) {
591                    let v = self.array[i].clone();
592                    self.set_int((i + 1) as i64, v)?;
593                }
594            }
595
596            self.alimit = old_asize;
597            std::mem::swap(&mut self.node, &mut new_hash_node);
598            std::mem::swap(&mut self.lsizenode, &mut new_hash_lsize);
599            std::mem::swap(&mut self.lastfree, &mut new_hash_lastfree);
600        }
601
602        self.array.resize_with(new_asize as usize, || LuaValue::Nil);
603
604        std::mem::swap(&mut self.node, &mut new_hash_node);
605        std::mem::swap(&mut self.lsizenode, &mut new_hash_lsize);
606        std::mem::swap(&mut self.lastfree, &mut new_hash_lastfree);
607        self.alimit = new_asize;
608
609        let old_hash_entries: Vec<(LuaValue, LuaValue)> = new_hash_node
610            .iter()
611            .filter(|n| !matches!(n.value, LuaValue::Nil))
612            .map(|n| (n.key_value(), n.value.clone()))
613            .collect();
614        drop(new_hash_node);
615        self.reinsert(old_hash_entries)?;
616
617        Ok(())
618    }
619
620    fn rehash(&mut self, extra_key: &LuaValue) -> Result<(), LuaError> {
621        let mut nums = [0u32; MAXABITS as usize + 1];
622        self.set_limit_to_size();
623
624        let na = self.num_use_array(&mut nums);
625        let mut na = na;
626        let mut totaluse = na as i32;
627
628        totaluse += self.num_use_hash(&mut nums, &mut na);
629
630        if let LuaValue::Int(ek) = extra_key {
631            if Self::count_int(*ek, &mut nums) { na += 1; }
632        }
633        totaluse += 1;
634
635        let asize = Self::compute_sizes(&nums, &mut na);
636
637        let nh = (totaluse - na as i32).max(0) as u32;
638        self.resize(asize, nh)
639    }
640
641    fn get_free_pos(&mut self) -> Option<usize> {
642        if self.is_dummy() { return None; }
643        loop {
644            let lf = self.lastfree?;
645            if lf == 0 {
646                self.lastfree = None;
647                return None;
648            }
649            let idx = lf - 1;
650            self.lastfree = Some(idx);
651            if self.node[idx].key_is_nil() && !self.node_has_chain_link(idx) {
652                return Some(idx);
653            }
654        }
655    }
656
657    fn node_has_chain_link(&self, idx: usize) -> bool {
658        if self.node[idx].next != 0 {
659            return true;
660        }
661        self.find_chain_predecessor(idx).is_some()
662    }
663
664    fn find_chain_predecessor(&self, idx: usize) -> Option<usize> {
665        self.node.iter().enumerate().find(|(prev, node)| {
666            node.next != 0 && (*prev as isize + node.next as isize) == idx as isize
667        }).map(|(prev, _)| prev)
668    }
669
670    fn clear_node(&mut self, idx: usize) {
671        self.node[idx].key = LuaValue::Nil;
672        self.node[idx].value = LuaValue::Nil;
673        self.node[idx].next = 0;
674    }
675
676    fn remove_hash_node(&mut self, idx: usize) {
677        if let Some(prev) = self.find_chain_predecessor(idx) {
678            let next = self.node[idx].next;
679            self.node[prev].next = if next == 0 {
680                0
681            } else {
682                let target = idx as isize + next as isize;
683                (target - prev as isize) as i32
684            };
685            self.clear_node(idx);
686            return;
687        }
688
689        let next = self.node[idx].next;
690        if next == 0 {
691            self.clear_node(idx);
692            return;
693        }
694
695        let next_idx = (idx as isize + next as isize) as usize;
696        let moved_next = self.node[next_idx].next;
697        let moved_key = self.node[next_idx].key_value();
698        let moved_value = self.node[next_idx].value.clone();
699        self.node[idx].key = moved_key;
700        self.node[idx].value = moved_value;
701        self.node[idx].next = if moved_next == 0 {
702            0
703        } else {
704            let target = next_idx as isize + moved_next as isize;
705            (target - idx as isize) as i32
706        };
707        self.clear_node(next_idx);
708    }
709
710    fn clear_dead_hash_node(&mut self, idx: usize) {
711        self.remove_hash_node(idx);
712    }
713
714    fn new_key(&mut self, key: &LuaValue, value: LuaValue) -> Result<(), LuaError> {
715        if matches!(key, LuaValue::Nil) {
716            return Err(LuaError::runtime(format_args!("table index is nil")));
717        }
718        let normalised_key;
719        let key = if let LuaValue::Float(f) = key {
720            let f = *f;
721            if f.is_nan() {
722                return Err(LuaError::runtime(format_args!("table index is NaN")));
723            }
724            let k = f as i64;
725            if k as f64 == f {
726                normalised_key = LuaValue::Int(k);
727                &normalised_key
728            } else { key }
729        } else { key };
730
731        if matches!(value, LuaValue::Nil) { return Ok(()); }
732
733        if self.is_dummy() && !matches!(key, LuaValue::Int(_)) {
734            self.set_node_vector(DUMMY_TABLE_INIT_HASH_NODES)?;
735            let mp = self.main_position(key);
736            self.node[mp].set_key(key);
737            self.node[mp].value = value;
738            return Ok(());
739        }
740
741        let mp = self.main_position(key);
742        let mp_occupied = self.is_dummy() || !matches!(self.node[mp].value, LuaValue::Nil);
743        if mp_occupied {
744            let f = self.get_free_pos();
745            let f = match f {
746                None => {
747                    self.rehash(key)?;
748                    return self.set(key, value);
749                }
750                Some(idx) => idx,
751            };
752
753            debug_assert!(!self.is_dummy());
754            let othern = self.main_position_from_node(mp);
755
756            if othern != mp {
757                let mut prev = othern;
758                while (prev as isize + self.node[prev].next as isize) as usize != mp {
759                    prev = (prev as isize + self.node[prev].next as isize) as usize;
760                }
761                self.node[prev].next = (f as isize - prev as isize) as i32;
762                let mp_key = self.node[mp].key_value();
763                let mp_val = self.node[mp].value.clone();
764                let mp_next = self.node[mp].next;
765                self.node[f].key = mp_key;
766                self.node[f].value = mp_val;
767                if mp_next != 0 {
768                    self.node[f].next = mp_next + (mp as isize - f as isize) as i32;
769                    self.node[mp].next = 0;
770                } else {
771                    self.node[f].next = 0;
772                }
773                self.node[mp].value = LuaValue::Nil;
774            } else {
775                if self.node[mp].next != 0 {
776                    let target = (mp as isize + self.node[mp].next as isize) as usize;
777                    self.node[f].next = (target as isize - f as isize) as i32;
778                } else {
779                    debug_assert!(self.node[f].next == 0);
780                }
781                self.node[mp].next = (f as isize - mp as isize) as i32;
782                self.node[f].set_key(key);
783                debug_assert!(matches!(self.node[f].value, LuaValue::Nil));
784                self.node[f].value = value;
785                return Ok(());
786            }
787        }
788        self.node[mp].set_key(key);
789        debug_assert!(matches!(self.node[mp].value, LuaValue::Nil));
790        self.node[mp].value = value;
791        Ok(())
792    }
793
794    fn get_int_slot(&self, key: i64) -> TableSlotRef {
795        let alimit = self.alimit as u64;
796        let uk = key as u64;
797        if uk.wrapping_sub(1) < alimit {
798            return TableSlotRef::Array((key - 1) as usize);
799        }
800        if !self.is_real_asize() && alimit > 0 {
801            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
802            if masked < alimit {
803                return TableSlotRef::Array((key - 1) as usize);
804            }
805        }
806        if self.is_dummy() { return TableSlotRef::Absent; }
807        let mut n = self.hash_idx_for_int(key);
808        loop {
809            if self.node[n].key_is_int() && self.node[n].key_int() == key {
810                return TableSlotRef::Hash(n);
811            }
812            let nx = self.node[n].next;
813            if nx == 0 { break; }
814            n = (n as isize + nx as isize) as usize;
815        }
816        TableSlotRef::Absent
817    }
818
819    /// Read an integer key directly to a [`LuaValue`], mirroring C's
820    /// `luaH_getint`. The array-part fast path returns the slot in a
821    /// single bounds-checked load without constructing an intermediate
822    /// [`TableSlotRef`] enum; only when the key falls through to the
823    /// hash part do we walk the chain. Equivalent in observable
824    /// behaviour to `slot_value(get_int_slot(key))`.
825    #[inline]
826    fn get_int_value(&self, key: i64) -> LuaValue {
827        let alimit = self.alimit as u64;
828        let uk = key as u64;
829        if uk.wrapping_sub(1) < alimit {
830            return self.array[(key - 1) as usize].clone();
831        }
832        self.get_int_value_cold(key)
833    }
834
835    #[cold]
836    #[inline(never)]
837    fn get_int_value_cold(&self, key: i64) -> LuaValue {
838        let alimit = self.alimit as u64;
839        let uk = key as u64;
840        if !self.is_real_asize() && alimit > 0 {
841            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
842            if masked < alimit {
843                return self.array[(key - 1) as usize].clone();
844            }
845        }
846        if self.is_dummy() { return LuaValue::Nil; }
847        let mut n = self.hash_idx_for_int(key);
848        loop {
849            if self.node[n].key_is_int() && self.node[n].key_int() == key {
850                return self.node[n].value.clone();
851            }
852            let nx = self.node[n].next;
853            if nx == 0 { break; }
854            n = (n as isize + nx as isize) as usize;
855        }
856        LuaValue::Nil
857    }
858
859    fn get_short_str_slot(&self, key: &GcRef<LuaString>) -> TableSlotRef {
860        debug_assert!(key.is_short());
861        if self.is_dummy() { return TableSlotRef::Absent; }
862        let mut n = self.hashpow2_idx(key.hash());
863        loop {
864            if self.node[n].key_is_short_str() {
865                let ks = self.node[n].key_string();
866                if GcRef::ptr_eq(ks, key) || ks.as_bytes() == key.as_bytes() {
867                    return TableSlotRef::Hash(n);
868                }
869            }
870            let nx = self.node[n].next;
871            if nx == 0 { return TableSlotRef::Absent; }
872            n = (n as isize + nx as isize) as usize;
873        }
874    }
875
876    /// Read a short-string key directly to a [`LuaValue`], mirroring the
877    /// shape of [`Self::get_int_value`]: a single hash-chain walk that
878    /// produces the slot's value without constructing an intermediate
879    /// [`TableSlotRef`] enum. Short strings are interned, so pointer
880    /// equality wins almost every comparison; the byte-equality fallback
881    /// handles the rare cross-interning-table path. Callers must ensure
882    /// `key.is_short()` before dispatching here.
883    #[inline]
884    fn get_str_value(&self, key: &GcRef<LuaString>) -> LuaValue {
885        debug_assert!(key.is_short());
886        if self.is_dummy() { return LuaValue::Nil; }
887        let mut n = self.hashpow2_idx(key.hash());
888        loop {
889            if self.node[n].key_is_short_str() {
890                let ks = self.node[n].key_string();
891                if GcRef::ptr_eq(ks, key) || ks.as_bytes() == key.as_bytes() {
892                    return self.node[n].value.clone();
893                }
894            }
895            let nx = self.node[n].next;
896            if nx == 0 { return LuaValue::Nil; }
897            n = (n as isize + nx as isize) as usize;
898        }
899    }
900
901    /// Cold fallback for keys that miss the integer- and short-string
902    /// fast paths in [`LuaTable::get`] (long strings, booleans,
903    /// non-integer floats, table / function keys, light userdata, …).
904    /// Routes through the existing `get_slot` + `slot_value` pair.
905    #[cold]
906    #[inline(never)]
907    fn get_generic_value(&self, key: &LuaValue) -> LuaValue {
908        let slot = self.get_slot(key);
909        self.slot_value(slot)
910    }
911
912    fn get_str_slot(&self, key: &GcRef<LuaString>) -> TableSlotRef {
913        if key.is_short() {
914            self.get_short_str_slot(key)
915        } else {
916            let ko = LuaValue::Str(key.clone());
917            self.get_generic_slot(&ko)
918        }
919    }
920
921    fn get_slot(&self, key: &LuaValue) -> TableSlotRef {
922        match key {
923            LuaValue::Str(s) if s.is_short() => self.get_short_str_slot(s),
924            LuaValue::Int(i) => self.get_int_slot(*i),
925            LuaValue::Nil => TableSlotRef::Absent,
926            LuaValue::Float(f) => {
927                let f = *f;
928                let k = f as i64;
929                if k as f64 == f { self.get_int_slot(k) }
930                else { self.get_generic_slot(key) }
931            }
932            _ => self.get_generic_slot(key),
933        }
934    }
935
936    fn slot_value(&self, slot: TableSlotRef) -> LuaValue {
937        match slot {
938            TableSlotRef::Array(i) => self.array[i].clone(),
939            TableSlotRef::Hash(i) => self.node[i].value.clone(),
940            TableSlotRef::Absent => LuaValue::Nil,
941        }
942    }
943
944    fn finish_set(&mut self, key: &LuaValue, slot: TableSlotRef, value: LuaValue) -> Result<(), LuaError> {
945        match slot {
946            TableSlotRef::Absent => self.new_key(key, value),
947            TableSlotRef::Array(i) => { self.array[i] = value; Ok(()) }
948            TableSlotRef::Hash(i) => { self.node[i].value = value; Ok(()) }
949        }
950    }
951
952    fn set(&mut self, key: &LuaValue, value: LuaValue) -> Result<(), LuaError> {
953        let slot = self.get_slot(key);
954        self.finish_set(key, slot, value)
955    }
956
957    /// Set by integer key. May grow the array part up to
958    /// [`ARRAY_GROW_CAP`] for keys just past `alimit` to amortise the
959    /// common `t[#t+1] = v` pattern.
960    fn set_int(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
961        let slot = self.get_int_slot(key);
962        if matches!(slot, TableSlotRef::Absent) {
963            if key > 0 && (key as u64) <= ARRAY_GROW_CAP as u64 {
964                let cur = self.alimit as i64;
965                if key == cur + 1 && !matches!(value, LuaValue::Nil) {
966                    let new_size = (key as u32).next_power_of_two().max(4);
967                    let capped = new_size.min(ARRAY_GROW_CAP);
968                    if capped > self.alimit {
969                        let nsize = self.alloc_sizenode();
970                        self.resize(capped, nsize)?;
971                        let new_slot = self.get_int_slot(key);
972                        return self.finish_set(&LuaValue::Int(key), new_slot, value);
973                    }
974                }
975            }
976        }
977        match slot {
978            TableSlotRef::Absent => {
979                let k = LuaValue::Int(key);
980                self.new_key(&k, value)
981            }
982            TableSlotRef::Array(i) => { self.array[i] = value; Ok(()) }
983            TableSlotRef::Hash(i) => { self.node[i].value = value; Ok(()) }
984        }
985    }
986
987    /// Integer-key entry used by [`LuaTable::try_raw_set`] /
988    /// [`LuaTable::try_raw_set_int`]. The array fast path writes
989    /// directly into a slot that is by definition already allocated
990    /// (it lives inside the `Vec` at offset `key-1 < alimit`), so the
991    /// `TOTAL_GROW_CAP` guard cannot apply. Only the cold path can
992    /// allocate; the guard runs there.
993    #[inline]
994    fn try_raw_set_int_fast(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
995        let alimit = self.alimit as u64;
996        let uk = key as u64;
997        if uk.wrapping_sub(1) < alimit {
998            self.array[(key - 1) as usize] = value;
999            return Ok(());
1000        }
1001        self.try_raw_set_int_cold(key, value)
1002    }
1003
1004    #[cold]
1005    #[inline(never)]
1006    fn try_raw_set_int_cold(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1007        if self.array.len() + self.node.len() >= TOTAL_GROW_CAP
1008            && matches!(self.get_int_slot(key), TableSlotRef::Absent)
1009        {
1010            return Err(LuaError::Memory);
1011        }
1012        self.set_int_value_cold(key, value)
1013    }
1014
1015    /// Cold fallback for [`Self::set_int_value`]: handles the
1016    /// alimit-aliased slot (non-real-`asize` tables), hash-part lookup
1017    /// + in-place store, the array-grow-on-`#t+1` heuristic, and
1018    /// `new_key` insertion. Split into a `#[cold] #[inline(never)]`
1019    /// helper so LLVM lays out the array fast path as straight-line
1020    /// code in the inlined caller.
1021    #[cold]
1022    #[inline(never)]
1023    fn set_int_value_cold(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1024        let alimit = self.alimit as u64;
1025        let uk = key as u64;
1026        if !self.is_real_asize() && alimit > 0 {
1027            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1028            if masked < alimit {
1029                self.array[(key - 1) as usize] = value;
1030                return Ok(());
1031            }
1032        }
1033        if !self.is_dummy() {
1034            let mut n = self.hash_idx_for_int(key);
1035            loop {
1036                if self.node[n].key_is_int() && self.node[n].key_int() == key {
1037                    self.node[n].value = value;
1038                    return Ok(());
1039                }
1040                let nx = self.node[n].next;
1041                if nx == 0 { break; }
1042                n = (n as isize + nx as isize) as usize;
1043            }
1044        }
1045        if key > 0 && (key as u64) <= ARRAY_GROW_CAP as u64 {
1046            let cur = self.alimit as i64;
1047            if key == cur + 1 && !matches!(value, LuaValue::Nil) {
1048                let new_size = (key as u32).next_power_of_two().max(4);
1049                let capped = new_size.min(ARRAY_GROW_CAP);
1050                if capped > self.alimit {
1051                    let nsize = self.alloc_sizenode();
1052                    self.resize(capped, nsize)?;
1053                    let new_slot = self.get_int_slot(key);
1054                    return self.finish_set(&LuaValue::Int(key), new_slot, value);
1055                }
1056            }
1057        }
1058        let k = LuaValue::Int(key);
1059        self.new_key(&k, value)
1060    }
1061
1062    // ── boundary search ────────────────────────────────────────────────────
1063
1064    fn hash_search(&self, mut j: u64) -> u64 {
1065        let mut i: u64;
1066        if j == 0 { j = 1; }
1067        loop {
1068            i = j;
1069            if j <= (i64::MAX as u64) / 2 {
1070                j *= 2;
1071            } else {
1072                j = i64::MAX as u64;
1073                let s = self.get_int_slot(j as i64);
1074                if matches!(s, TableSlotRef::Absent)
1075                    || matches!(self.slot_value(s), LuaValue::Nil)
1076                {
1077                    break;
1078                } else { return j; }
1079            }
1080            let s = self.get_int_slot(j as i64);
1081            if matches!(s, TableSlotRef::Absent) { break; }
1082            if matches!(self.slot_value(s), LuaValue::Nil) { break; }
1083        }
1084        while j - i > 1 {
1085            let m = i / 2 + j / 2;
1086            let s = self.get_int_slot(m as i64);
1087            let empty = matches!(s, TableSlotRef::Absent)
1088                || matches!(self.slot_value(s), LuaValue::Nil);
1089            if empty { j = m; } else { i = m; }
1090        }
1091        i
1092    }
1093
1094    fn bin_search(array: &[LuaValue], mut i: u32, mut j: u32) -> u32 {
1095        while j - i > 1 {
1096            let m = (i + j) / 2;
1097            if matches!(array[(m - 1) as usize], LuaValue::Nil) { j = m; }
1098            else { i = m; }
1099        }
1100        i
1101    }
1102
1103    /// Find a boundary `i` such that `t[i]` is present and `t[i+1]` is absent,
1104    /// or 0 if `t[1]` is absent. C: `luaH_getn`.
1105    fn getn(&mut self) -> u64 {
1106        let limit = self.alimit;
1107        if limit > 0 && matches!(self.array[(limit - 1) as usize], LuaValue::Nil) {
1108            if limit >= 2 && !matches!(self.array[(limit - 2) as usize], LuaValue::Nil) {
1109                if self.is_pow2_real_asize() && !Self::is_pow2(limit - 1) {
1110                    self.alimit = limit - 1;
1111                    self.flags.set_no_real_asize();
1112                }
1113                return (limit - 1) as u64;
1114            } else {
1115                let boundary = Self::bin_search(&self.array, 0, limit);
1116                if self.is_pow2_real_asize() && boundary > self.real_asize() / 2 {
1117                    self.alimit = boundary;
1118                    self.flags.set_no_real_asize();
1119                }
1120                return boundary as u64;
1121            }
1122        }
1123        if !self.limit_equals_asize() {
1124            if matches!(self.array[limit as usize], LuaValue::Nil) {
1125                return limit as u64;
1126            }
1127            let real = self.real_asize();
1128            if matches!(self.array[(real - 1) as usize], LuaValue::Nil) {
1129                let old_alimit = self.alimit;
1130                let boundary = Self::bin_search(&self.array, old_alimit, real);
1131                self.alimit = boundary;
1132                return boundary as u64;
1133            }
1134        }
1135        let limit = self.real_asize();
1136        debug_assert!(
1137            limit == self.real_asize()
1138                && (limit == 0 || !matches!(self.array[(limit - 1) as usize], LuaValue::Nil))
1139        );
1140        let next_key = (limit as i64).saturating_add(1);
1141        let next_slot = self.get_int_slot(next_key);
1142        let next_empty = matches!(next_slot, TableSlotRef::Absent)
1143            || matches!(self.slot_value(next_slot), LuaValue::Nil);
1144        if self.is_dummy() || next_empty {
1145            return limit as u64;
1146        }
1147        self.hash_search(limit as u64)
1148    }
1149}
1150
1151// ── LuaTable (outer handle) ────────────────────────────────────────────────────
1152
1153/// A Lua table: hybrid array + hash map.
1154///
1155/// All public methods take `&self` so the type works through
1156/// `GcRef<LuaTable>` (which only dereferences to a shared borrow).
1157/// Mutations are routed through an internal [`RefCell`].
1158#[derive(Debug)]
1159pub struct LuaTable {
1160    inner: RefCell<TableInner>,
1161    metatable: RefCell<Option<GcRef<LuaTable>>>,
1162    weak_mode: Cell<u8>,
1163}
1164
1165impl std::fmt::Debug for TableInner {
1166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1167        f.debug_struct("TableInner")
1168            .field("alimit", &self.alimit)
1169            .field("array_len", &self.array.len())
1170            .field("node_len", &self.node.len())
1171            .finish()
1172    }
1173}
1174
1175impl Default for LuaTable {
1176    fn default() -> Self {
1177        LuaTable {
1178            inner: RefCell::new(TableInner::new()),
1179            metatable: RefCell::new(None),
1180            weak_mode: Cell::new(0),
1181        }
1182    }
1183}
1184
1185impl LuaTable {
1186    /// Construct an empty table. Used as a placeholder by callers that
1187    /// will populate it via the normal API.
1188    pub fn placeholder() -> Self { Self::default() }
1189
1190    /// Borrow inner storage for read access. Intended for advanced
1191    /// callers (e.g. the GC trace impl); prefer the typed methods.
1192    pub fn with_inner<R>(&self, f: impl FnOnce(&TableInner) -> R) -> R {
1193        f(&self.inner.borrow())
1194    }
1195
1196    /// Read a key. Returns `LuaValue::Nil` if absent or if `k` is nil.
1197    /// Integer keys take the direct array-part fast path used by
1198    /// [`LuaTable::get_int`]; short-string keys take the analogous
1199    /// hash-chain fast path used by [`LuaTable::get_short_str`]; every
1200    /// other key shape falls through to the cold generic slot lookup.
1201    /// Marked `#[inline(always)]` so the dispatch folds into the
1202    /// caller (the hot `state::fast_get` / `state::table_get_with_tm`
1203    /// frames in the VM); profiling at #[inline] showed LLVM was still
1204    /// emitting a cross-crate function call here.
1205    #[inline(always)]
1206    pub fn get(&self, k: &LuaValue) -> LuaValue {
1207        let inner = self.inner.borrow();
1208        match k {
1209            LuaValue::Nil => LuaValue::Nil,
1210            LuaValue::Int(i) => inner.get_int_value(*i),
1211            LuaValue::Str(s) if s.is_short() => inner.get_str_value(s),
1212            _ => inner.get_generic_value(k),
1213        }
1214    }
1215
1216    /// Read by integer key. Hot path: callers like `state.fast_get_int`
1217    /// and `state.table_get_with_tm` dispatch here on every integer-key
1218    /// access in user code (`t[1]`, `OP_GETI`, ipairs loops, etc.). The
1219    /// array-part lookup folds into a single bounds-checked load,
1220    /// matching C's `luaH_getint`.
1221    #[inline(always)]
1222    pub fn get_int(&self, key: i64) -> LuaValue {
1223        let inner = self.inner.borrow();
1224        inner.get_int_value(key)
1225    }
1226
1227    /// Read by string key. Despite the name (kept for compatibility
1228    /// with the old API), this dispatches internally to either the
1229    /// short- or long-string path; passing a long string is safe. The
1230    /// short-string branch (the common case — all interned identifiers
1231    /// and most table-field keys are short) takes the folded hash-walk
1232    /// in [`TableInner::get_str_value`]; long strings still go through
1233    /// the slot indirection.
1234    #[inline(always)]
1235    pub fn get_short_str(&self, k: &GcRef<LuaString>) -> LuaValue {
1236        let inner = self.inner.borrow();
1237        if k.is_short() {
1238            inner.get_str_value(k)
1239        } else {
1240            let slot = inner.get_str_slot(k);
1241            inner.slot_value(slot)
1242        }
1243    }
1244
1245    /// Read by raw byte-string key. Linear scan over the hash part —
1246    /// rarely-used helper for callers that don't have a `GcRef<LuaString>`
1247    /// handle.
1248    pub fn get_str_bytes(&self, key_bytes: &[u8]) -> LuaValue {
1249        let mut found = LuaValue::Nil;
1250        self.for_each_entry(|k, v| {
1251            if !matches!(found, LuaValue::Nil) { return; }
1252            if let LuaValue::Str(s) = k {
1253                if s.as_bytes() == key_bytes {
1254                    found = v.clone();
1255                }
1256            }
1257        });
1258        found
1259    }
1260
1261    /// Raw set without metamethod dispatch. Nil keys are an error;
1262    /// NaN-float keys are an error. Setting `v == Nil` clears the slot.
1263    pub fn raw_set(&self, k: LuaValue, v: LuaValue) {
1264        if matches!(k, LuaValue::Nil) { return; }
1265        if let LuaValue::Float(f) = &k {
1266            if f.is_nan() { return; }
1267        }
1268        let mut inner = self.inner.borrow_mut();
1269        let _ = inner.set(&k, v);
1270    }
1271
1272    /// Raw set with explicit error returns; preferred path used by
1273    /// `LuaTableRefExt::raw_set` in `lua-vm`. Integer keys (and floats
1274    /// that are exact integers) take the same direct array-part fast
1275    /// path used by [`LuaTable::try_raw_set_int`]; other key shapes
1276    /// fall through to the generic slot lookup.
1277    #[inline]
1278    pub fn try_raw_set(&self, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1279        match &k {
1280            LuaValue::Nil => {
1281                Err(LuaError::runtime(format_args!("table index is nil")))
1282            }
1283            LuaValue::Float(f) if f.is_nan() => {
1284                Err(LuaError::runtime(format_args!("table index is NaN")))
1285            }
1286            LuaValue::Int(i) => {
1287                let key = *i;
1288                let mut inner = self.inner.borrow_mut();
1289                inner.try_raw_set_int_fast(key, v)
1290            }
1291            LuaValue::Float(f) => {
1292                let f = *f;
1293                let k_int = f as i64;
1294                if k_int as f64 == f {
1295                    let mut inner = self.inner.borrow_mut();
1296                    inner.try_raw_set_int_fast(k_int, v)
1297                } else {
1298                    self.try_raw_set_generic(k, v)
1299                }
1300            }
1301            _ => self.try_raw_set_generic(k, v),
1302        }
1303    }
1304
1305    /// Generic-key path for [`Self::try_raw_set`]. Split out so the
1306    /// integer fast path stays branch-light and inlineable.
1307    #[cold]
1308    #[inline(never)]
1309    fn try_raw_set_generic(&self, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1310        let mut inner = self.inner.borrow_mut();
1311        if inner.array.len() + inner.node.len() >= TOTAL_GROW_CAP
1312            && matches!(inner.get_slot(&k), TableSlotRef::Absent)
1313        {
1314            return Err(LuaError::Memory);
1315        }
1316        inner.set(&k, v)
1317    }
1318
1319    /// Raw set by integer key with explicit error returns. Routes the
1320    /// array-part fast path through [`TableInner::set_int_value`] — a
1321    /// single bounds-checked store with no intermediate
1322    /// [`TableSlotRef`] indirection — and only consults the
1323    /// `TOTAL_GROW_CAP` allocation guard when the key would create a
1324    /// new slot.
1325    #[inline]
1326    pub fn try_raw_set_int(&self, k: i64, v: LuaValue) -> Result<(), LuaError> {
1327        let mut inner = self.inner.borrow_mut();
1328        inner.try_raw_set_int_fast(k, v)
1329    }
1330
1331    /// Resize the table to new array and hash sizes (sizing hint from
1332    /// the bytecode's `OP_NEWTABLE`).
1333    pub fn resize(&self, new_asize: u32, new_hsize: u32) -> Result<(), LuaError> {
1334        let mut inner = self.inner.borrow_mut();
1335        inner.resize(new_asize, new_hsize)
1336    }
1337
1338    /// Number of array-part slots currently allocated. Cheap counter
1339    /// for sizing decisions; NOT the Lua `#t` length operator.
1340    pub fn array_len(&self) -> usize { self.inner.borrow().array.len() }
1341
1342    /// Total occupied slots (array + hash) — used for legacy
1343    /// `len()` callers; prefer `getn` for the Lua `#` operator.
1344    pub fn len(&self) -> usize {
1345        let inner = self.inner.borrow();
1346        let mut n = 0usize;
1347        for v in inner.array.iter() {
1348            if !matches!(v, LuaValue::Nil) { n += 1; }
1349        }
1350        for node in inner.node.iter() {
1351            if !matches!(node.value, LuaValue::Nil) { n += 1; }
1352        }
1353        n
1354    }
1355    pub fn is_empty(&self) -> bool { self.len() == 0 }
1356
1357    /// `#t` boundary (C: `luaH_getn`). Mutates internal caching state.
1358    pub fn getn(&self) -> u64 {
1359        let mut inner = self.inner.borrow_mut();
1360        inner.getn()
1361    }
1362
1363    /// Returns true iff `k` resolves to a slot in this table (array or
1364    /// hash). Used by `next` to validate the resumption key.
1365    pub fn contains_key(&self, k: &LuaValue) -> bool {
1366        if matches!(k, LuaValue::Nil) { return false; }
1367        let inner = self.inner.borrow();
1368        let slot = inner.get_slot(k);
1369        !matches!(slot, TableSlotRef::Absent)
1370    }
1371
1372    pub fn metatable(&self) -> Option<GcRef<LuaTable>> {
1373        self.metatable.borrow().clone()
1374    }
1375
1376    /// Install a metatable. Inspects its `__mode` field eagerly so the
1377    /// GC trace impl can read [`weak_mode`] without re-entering the
1378    /// metatable RefCell.
1379    pub fn set_metatable(&self, mt: Option<GcRef<LuaTable>>) {
1380        let mode = mt.as_ref().map(|t| extract_weak_mode(t)).unwrap_or(0);
1381        self.weak_mode.set(mode);
1382        *self.metatable.borrow_mut() = mt;
1383    }
1384
1385    pub fn weak_mode(&self) -> u8 { self.weak_mode.get() }
1386
1387    /// Implements Lua's `next(t, k)`.
1388    pub fn next_pair(&self, k: &LuaValue) -> Option<(LuaValue, LuaValue)> {
1389        let inner = self.inner.borrow();
1390        inner.next_pair(k).ok().flatten()
1391    }
1392
1393    /// Like [`next_pair`] but reports the `"invalid key to 'next'"`
1394    /// error when `k` is non-nil and not present.
1395    pub fn try_next_pair(&self, k: &LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
1396        let inner = self.inner.borrow();
1397        inner.next_pair(k)
1398    }
1399
1400    /// Walk every live (key, value) pair via the given closure.
1401    /// Used by the GC trace impl to avoid the overhead of repeatedly
1402    /// re-entering `find_index` from `next_pair`.
1403    pub fn for_each_entry(&self, mut f: impl FnMut(&LuaValue, &LuaValue)) {
1404        let inner = self.inner.borrow();
1405        for (i, v) in inner.array.iter().enumerate() {
1406            if !matches!(v, LuaValue::Nil) {
1407                let k = LuaValue::Int((i + 1) as i64);
1408                f(&k, v);
1409            }
1410        }
1411        for node in inner.node.iter() {
1412            if !matches!(node.value, LuaValue::Nil) {
1413                f(&node.key, &node.value);
1414            }
1415        }
1416    }
1417
1418    /// Drop weak entries whose weakly-tracked target is unreachable,
1419    /// and return the list of values whose strings must still be
1420    /// marked by the caller.
1421    pub fn prune_weak_dead(&self, is_reachable: &dyn Fn(usize) -> bool) -> Vec<LuaValue> {
1422        let mode = self.weak_mode.get();
1423        if mode == 0 { return Vec::new(); }
1424        let weak_k = (mode & WEAK_KEYS) != 0;
1425        let weak_v = (mode & WEAK_VALUES) != 0;
1426        let mut to_mark: Vec<LuaValue> = Vec::new();
1427        let mut inner = self.inner.borrow_mut();
1428        for i in 0..inner.array.len() {
1429            let v = inner.array[i].clone();
1430            if matches!(v, LuaValue::Nil) { continue; }
1431            if weak_v && value_is_dead_collectable(&v, is_reachable) {
1432                inner.array[i] = LuaValue::Nil;
1433                continue;
1434            }
1435            if weak_v {
1436                if matches!(v, LuaValue::Str(_)) { to_mark.push(v); }
1437            }
1438        }
1439        let mut i = 0;
1440        while i < inner.node.len() {
1441            let v = inner.node[i].value.clone();
1442            if matches!(v, LuaValue::Nil) {
1443                i += 1;
1444                continue;
1445            }
1446            let k = inner.node[i].key.clone();
1447            if weak_v && value_is_dead_collectable(&v, is_reachable) {
1448                inner.clear_dead_hash_node(i);
1449                continue;
1450            }
1451            if weak_k && value_is_dead_collectable(&k, is_reachable) {
1452                inner.clear_dead_hash_node(i);
1453                continue;
1454            }
1455            if weak_k {
1456                if matches!(k, LuaValue::Str(_)) { to_mark.push(k); }
1457            }
1458            if weak_v {
1459                if matches!(v, LuaValue::Str(_)) { to_mark.push(v); }
1460            }
1461            i += 1;
1462        }
1463        to_mark
1464    }
1465
1466    /// Ephemeron-convergence helper for pure `__mode = "k"` tables.
1467    pub fn ephemeron_values_to_mark(&self, is_reachable: &dyn Fn(usize) -> bool) -> Vec<LuaValue> {
1468        let mode = self.weak_mode.get();
1469        if (mode & WEAK_KEYS) == 0 || (mode & WEAK_VALUES) != 0 {
1470            return Vec::new();
1471        }
1472        let inner = self.inner.borrow();
1473        let mut out = Vec::new();
1474        for node in inner.node.iter() {
1475            if matches!(node.value, LuaValue::Nil) { continue; }
1476            if !value_is_dead_collectable(&node.key, is_reachable) {
1477                out.push(node.value.clone());
1478            }
1479        }
1480        for (i, v) in inner.array.iter().enumerate() {
1481            if matches!(v, LuaValue::Nil) { continue; }
1482            let k = LuaValue::Int((i + 1) as i64);
1483            if !value_is_dead_collectable(&k, is_reachable) {
1484                out.push(v.clone());
1485            }
1486        }
1487        out
1488    }
1489}
1490
1491// ── Free helpers ──────────────────────────────────────────────────────────────
1492
1493/// True iff `v` is a collectable non-string LuaValue whose target was
1494/// unreached during the mark phase. Strings are explicitly excluded.
1495fn value_is_dead_collectable(v: &LuaValue, is_reachable: &dyn Fn(usize) -> bool) -> bool {
1496    match v {
1497        LuaValue::Table(t) => !is_reachable(t.identity()),
1498        LuaValue::UserData(u) => !is_reachable(u.identity()),
1499        LuaValue::Thread(th) => !is_reachable(th.identity()),
1500        LuaValue::Function(c) => match c {
1501            LuaClosure::Lua(x) => !is_reachable(x.identity()),
1502            LuaClosure::C(x) => !is_reachable(x.identity()),
1503            LuaClosure::LightC(_) => false,
1504        },
1505        LuaValue::Str(_)
1506        | LuaValue::Nil
1507        | LuaValue::Bool(_)
1508        | LuaValue::Int(_)
1509        | LuaValue::Float(_)
1510        | LuaValue::LightUserData(_) => false,
1511    }
1512}
1513
1514/// Inspect a metatable's `__mode` field and produce the corresponding
1515/// `WEAK_KEYS | WEAK_VALUES` bitmask. Returns 0 when no `__mode` is
1516/// set or it is not a string.
1517fn extract_weak_mode(mt: &LuaTable) -> u8 {
1518    let inner = mt.inner.borrow();
1519    for node in inner.node.iter() {
1520        if let LuaValue::Str(ks) = &node.key {
1521            if ks.as_bytes() == b"__mode" {
1522                if let LuaValue::Str(vs) = &node.value {
1523                    let bytes = vs.as_bytes();
1524                    let mut mode = 0u8;
1525                    if bytes.iter().any(|b| *b == b'k') { mode |= WEAK_KEYS; }
1526                    if bytes.iter().any(|b| *b == b'v') { mode |= WEAK_VALUES; }
1527                    return mode;
1528                }
1529                return 0;
1530            }
1531        }
1532    }
1533    0
1534}
1535
1536// ──────────────────────────────────────────────────────────────────────────────
1537// PORT STATUS
1538//   source:        src/ltable.c (~995 lines, 28 functions), src/ltable.h
1539//   target_crate:  lua-types
1540//   confidence:    high
1541//   todos:         0
1542//   port_notes:    0
1543//   unsafe_blocks: 0
1544//   notes:         Canonical LuaTable: hybrid array + hash. Mirrors C's Table
1545//                  struct (flags, lsizenode, alimit, array, node, lastfree) with
1546//                  Vec<LuaValue> + Vec<TableNode> in place of raw C pointers, and
1547//                  Option<usize> indexing in place of Node*. The luaH_getn
1548//                  boundary search + alimit-aware integer-key fast path are
1549//                  ported faithfully (see getn() and get_int_slot()). The
1550//                  integer-key read path also exposes get_int_value, which
1551//                  mirrors C's luaH_getint by returning the array slot directly
1552//                  in one bounds-checked load (no TableSlotRef indirection) and
1553//                  splitting the rare alimit-aliased / hash-part path into a
1554//                  cold helper. The short-string read path mirrors that shape
1555//                  via get_str_value (single hash-chain walk, no TableSlotRef
1556//                  round-trip); LuaTable::get dispatches on integer/short-string
1557//                  keys inline and routes everything else through a #[cold]
1558//                  get_generic_value, matching C's luaH_get fast-path structure.
1559//                  LuaTable::get / get_int / get_short_str are #[inline(always)]
1560//                  so the dispatch folds into the cross-crate VM hot frames
1561//                  (state::fast_get / state::table_get_with_tm). Weak-table mode
1562//                  flags + the prune_weak_dead / ephemeron_values_to_mark
1563//                  helpers integrate with the lua-gc Trace impl.
1564// ──────────────────────────────────────────────────────────────────────────────