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