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