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