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