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