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    #[inline(always)]
962    fn get_short_str_slot(&self, key: &GcRef<LuaString>) -> TableSlotRef {
963        debug_assert!(key.is_short());
964        if self.is_dummy() {
965            return TableSlotRef::Absent;
966        }
967        let mut n = self.hashpow2_idx(key.hash());
968        loop {
969            if self.node[n].key_is_short_str() {
970                let ks = self.node[n].key_string();
971                if lua_string_content_eq(ks, key) {
972                    return TableSlotRef::Hash(n);
973                }
974            }
975            let nx = self.node[n].next;
976            if nx == 0 {
977                return TableSlotRef::Absent;
978            }
979            n = (n as isize + nx as isize) as usize;
980        }
981    }
982
983    #[inline(always)]
984    fn try_update_short_str(
985        &mut self,
986        key: &GcRef<LuaString>,
987        value: LuaValue,
988    ) -> Result<(), LuaValue> {
989        debug_assert!(key.is_short());
990        if self.is_dummy() {
991            return Err(value);
992        }
993        let mut n = self.hashpow2_idx(key.hash());
994        loop {
995            if self.node[n].key_is_short_str() {
996                let ks = self.node[n].key_string();
997                if lua_string_content_eq(ks, key) {
998                    self.node[n].value = value;
999                    return Ok(());
1000                }
1001            }
1002            let nx = self.node[n].next;
1003            if nx == 0 {
1004                return Err(value);
1005            }
1006            n = (n as isize + nx as isize) as usize;
1007        }
1008    }
1009
1010    #[inline(always)]
1011    fn try_update_int(&mut self, key: i64, value: LuaValue) -> Result<(), LuaValue> {
1012        let alimit = self.alimit as u64;
1013        let uk = key as u64;
1014        if uk.wrapping_sub(1) < alimit {
1015            self.array[(key - 1) as usize] = value;
1016            return Ok(());
1017        }
1018        if !self.is_real_asize() && alimit > 0 {
1019            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1020            if masked < alimit {
1021                self.array[(key - 1) as usize] = value;
1022                return Ok(());
1023            }
1024        }
1025        if !self.is_dummy() {
1026            let mut n = self.hash_idx_for_int(key);
1027            loop {
1028                if self.node[n].key_is_int() && self.node[n].key_int() == key {
1029                    self.node[n].value = value;
1030                    return Ok(());
1031                }
1032                let nx = self.node[n].next;
1033                if nx == 0 {
1034                    break;
1035                }
1036                n = (n as isize + nx as isize) as usize;
1037            }
1038        }
1039        Err(value)
1040    }
1041
1042    /// Read a short-string key directly to a [`LuaValue`], mirroring the
1043    /// shape of [`Self::get_int_value`]: a single hash-chain walk that
1044    /// produces the slot's value without constructing an intermediate
1045    /// [`TableSlotRef`] enum. Short strings are interned, so pointer
1046    /// equality wins almost every comparison; the byte-equality fallback
1047    /// handles the rare cross-interning-table path. Callers must ensure
1048    /// `key.is_short()` before dispatching here.
1049    #[inline]
1050    fn get_str_value(&self, key: &GcRef<LuaString>) -> LuaValue {
1051        debug_assert!(key.is_short());
1052        if self.is_dummy() {
1053            return LuaValue::Nil;
1054        }
1055        let mut n = self.hashpow2_idx(key.hash());
1056        loop {
1057            if self.node[n].key_is_short_str() {
1058                let ks = self.node[n].key_string();
1059                if lua_string_content_eq(ks, key) {
1060                    return self.node[n].value.clone();
1061                }
1062            }
1063            let nx = self.node[n].next;
1064            if nx == 0 {
1065                return LuaValue::Nil;
1066            }
1067            n = (n as isize + nx as isize) as usize;
1068        }
1069    }
1070
1071    /// Cold fallback for keys that miss the integer- and short-string
1072    /// fast paths in [`LuaTable::get`] (long strings, booleans,
1073    /// non-integer floats, table / function keys, light userdata, …).
1074    /// Routes through the existing `get_slot` + `slot_value` pair.
1075    #[cold]
1076    #[inline(never)]
1077    fn get_generic_value(&self, key: &LuaValue) -> LuaValue {
1078        let slot = self.get_slot(key);
1079        self.slot_value(slot)
1080    }
1081
1082    fn get_str_slot(&self, key: &GcRef<LuaString>) -> TableSlotRef {
1083        if key.is_short() {
1084            self.get_short_str_slot(key)
1085        } else {
1086            let ko = LuaValue::Str(key.clone());
1087            self.get_generic_slot(&ko)
1088        }
1089    }
1090
1091    fn get_slot(&self, key: &LuaValue) -> TableSlotRef {
1092        match key {
1093            LuaValue::Str(s) if s.is_short() => self.get_short_str_slot(s),
1094            LuaValue::Int(i) => self.get_int_slot(*i),
1095            LuaValue::Nil => TableSlotRef::Absent,
1096            LuaValue::Float(f) => {
1097                let f = *f;
1098                let k = f as i64;
1099                if k as f64 == f {
1100                    self.get_int_slot(k)
1101                } else {
1102                    self.get_generic_slot(key)
1103                }
1104            }
1105            _ => self.get_generic_slot(key),
1106        }
1107    }
1108
1109    fn slot_value(&self, slot: TableSlotRef) -> LuaValue {
1110        match slot {
1111            TableSlotRef::Array(i) => self.array[i].clone(),
1112            TableSlotRef::Hash(i) => self.node[i].value.clone(),
1113            TableSlotRef::Absent => LuaValue::Nil,
1114        }
1115    }
1116
1117    fn finish_set(
1118        &mut self,
1119        key: &LuaValue,
1120        slot: TableSlotRef,
1121        value: LuaValue,
1122    ) -> Result<(), LuaError> {
1123        match slot {
1124            TableSlotRef::Absent => self.new_key(key, value),
1125            TableSlotRef::Array(i) => {
1126                self.array[i] = value;
1127                Ok(())
1128            }
1129            TableSlotRef::Hash(i) => {
1130                self.node[i].value = value;
1131                Ok(())
1132            }
1133        }
1134    }
1135
1136    fn set(&mut self, key: &LuaValue, value: LuaValue) -> Result<(), LuaError> {
1137        let slot = self.get_slot(key);
1138        self.finish_set(key, slot, value)
1139    }
1140
1141    /// Set by integer key. May grow the array part up to
1142    /// [`ARRAY_GROW_CAP`] for keys just past `alimit` to amortise the
1143    /// common `t[#t+1] = v` pattern.
1144    fn set_int(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1145        let slot = self.get_int_slot(key);
1146        if matches!(slot, TableSlotRef::Absent) {
1147            if key > 0 && (key as u64) <= ARRAY_GROW_CAP as u64 {
1148                let cur = self.alimit as i64;
1149                if key == cur + 1 && !matches!(value, LuaValue::Nil) {
1150                    let new_size = (key as u32).next_power_of_two().max(4);
1151                    let capped = new_size.min(ARRAY_GROW_CAP);
1152                    if capped > self.alimit {
1153                        let nsize = self.alloc_sizenode();
1154                        self.resize(capped, nsize)?;
1155                        let new_slot = self.get_int_slot(key);
1156                        return self.finish_set(&LuaValue::Int(key), new_slot, value);
1157                    }
1158                }
1159            }
1160        }
1161        match slot {
1162            TableSlotRef::Absent => {
1163                let k = LuaValue::Int(key);
1164                self.new_key(&k, value)
1165            }
1166            TableSlotRef::Array(i) => {
1167                self.array[i] = value;
1168                Ok(())
1169            }
1170            TableSlotRef::Hash(i) => {
1171                self.node[i].value = value;
1172                Ok(())
1173            }
1174        }
1175    }
1176
1177    /// Integer-key entry used by [`LuaTable::try_raw_set`] /
1178    /// [`LuaTable::try_raw_set_int`]. The array fast path writes
1179    /// directly into a slot that is by definition already allocated
1180    /// (it lives inside the `Vec` at offset `key-1 < alimit`), so the
1181    /// `TOTAL_GROW_CAP` guard cannot apply. Only the cold path can
1182    /// allocate; the guard runs there.
1183    #[inline]
1184    fn try_raw_set_int_fast(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1185        let alimit = self.alimit as u64;
1186        let uk = key as u64;
1187        if uk.wrapping_sub(1) < alimit {
1188            self.array[(key - 1) as usize] = value;
1189            return Ok(());
1190        }
1191        self.try_raw_set_int_cold(key, value)
1192    }
1193
1194    #[cold]
1195    #[inline(never)]
1196    fn try_raw_set_int_cold(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1197        if self.array.len() + self.node.len() >= TOTAL_GROW_CAP
1198            && matches!(self.get_int_slot(key), TableSlotRef::Absent)
1199        {
1200            return Err(LuaError::Memory);
1201        }
1202        self.set_int_value_cold(key, value)
1203    }
1204
1205    /// Cold fallback for [`Self::set_int_value`]: handles the
1206    /// alimit-aliased slot (non-real-`asize` tables), hash-part lookup
1207    /// + in-place store, the array-grow-on-`#t+1` heuristic, and
1208    /// `new_key` insertion. Split into a `#[cold] #[inline(never)]`
1209    /// helper so LLVM lays out the array fast path as straight-line
1210    /// code in the inlined caller.
1211    #[cold]
1212    #[inline(never)]
1213    fn set_int_value_cold(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1214        let alimit = self.alimit as u64;
1215        let uk = key as u64;
1216        if !self.is_real_asize() && alimit > 0 {
1217            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1218            if masked < alimit {
1219                self.array[(key - 1) as usize] = value;
1220                return Ok(());
1221            }
1222        }
1223        if !self.is_dummy() {
1224            let mut n = self.hash_idx_for_int(key);
1225            loop {
1226                if self.node[n].key_is_int() && self.node[n].key_int() == key {
1227                    self.node[n].value = value;
1228                    return Ok(());
1229                }
1230                let nx = self.node[n].next;
1231                if nx == 0 {
1232                    break;
1233                }
1234                n = (n as isize + nx as isize) as usize;
1235            }
1236        }
1237        if key > 0 && (key as u64) <= ARRAY_GROW_CAP as u64 {
1238            let cur = self.alimit as i64;
1239            if key == cur + 1 && !matches!(value, LuaValue::Nil) {
1240                let new_size = (key as u32).next_power_of_two().max(4);
1241                let capped = new_size.min(ARRAY_GROW_CAP);
1242                if capped > self.alimit {
1243                    let nsize = self.alloc_sizenode();
1244                    self.resize(capped, nsize)?;
1245                    let new_slot = self.get_int_slot(key);
1246                    return self.finish_set(&LuaValue::Int(key), new_slot, value);
1247                }
1248            }
1249        }
1250        let k = LuaValue::Int(key);
1251        self.new_key(&k, value)
1252    }
1253
1254    // ── boundary search ────────────────────────────────────────────────────
1255
1256    fn hash_search(&self, mut j: u64) -> u64 {
1257        let mut i: u64;
1258        if j == 0 {
1259            j = 1;
1260        }
1261        loop {
1262            i = j;
1263            if j <= (i64::MAX as u64) / 2 {
1264                j *= 2;
1265            } else {
1266                j = i64::MAX as u64;
1267                let s = self.get_int_slot(j as i64);
1268                if matches!(s, TableSlotRef::Absent) || matches!(self.slot_value(s), LuaValue::Nil)
1269                {
1270                    break;
1271                } else {
1272                    return j;
1273                }
1274            }
1275            let s = self.get_int_slot(j as i64);
1276            if matches!(s, TableSlotRef::Absent) {
1277                break;
1278            }
1279            if matches!(self.slot_value(s), LuaValue::Nil) {
1280                break;
1281            }
1282        }
1283        while j - i > 1 {
1284            let m = i / 2 + j / 2;
1285            let s = self.get_int_slot(m as i64);
1286            let empty =
1287                matches!(s, TableSlotRef::Absent) || matches!(self.slot_value(s), LuaValue::Nil);
1288            if empty {
1289                j = m;
1290            } else {
1291                i = m;
1292            }
1293        }
1294        i
1295    }
1296
1297    fn bin_search(array: &[LuaValue], mut i: u32, mut j: u32) -> u32 {
1298        while j - i > 1 {
1299            let m = (i + j) / 2;
1300            if matches!(array[(m - 1) as usize], LuaValue::Nil) {
1301                j = m;
1302            } else {
1303                i = m;
1304            }
1305        }
1306        i
1307    }
1308
1309    /// Find a boundary `i` such that `t[i]` is present and `t[i+1]` is absent,
1310    /// or 0 if `t[1]` is absent. C: `luaH_getn`.
1311    fn getn(&mut self) -> u64 {
1312        let limit = self.alimit;
1313        if limit > 0 && matches!(self.array[(limit - 1) as usize], LuaValue::Nil) {
1314            if limit >= 2 && !matches!(self.array[(limit - 2) as usize], LuaValue::Nil) {
1315                if self.is_pow2_real_asize() && !Self::is_pow2(limit - 1) {
1316                    self.alimit = limit - 1;
1317                    self.flags.set_no_real_asize();
1318                }
1319                return (limit - 1) as u64;
1320            } else {
1321                let boundary = Self::bin_search(&self.array, 0, limit);
1322                if self.is_pow2_real_asize() && boundary > self.real_asize() / 2 {
1323                    self.alimit = boundary;
1324                    self.flags.set_no_real_asize();
1325                }
1326                return boundary as u64;
1327            }
1328        }
1329        if !self.limit_equals_asize() {
1330            if matches!(self.array[limit as usize], LuaValue::Nil) {
1331                return limit as u64;
1332            }
1333            let real = self.real_asize();
1334            if matches!(self.array[(real - 1) as usize], LuaValue::Nil) {
1335                let old_alimit = self.alimit;
1336                let boundary = Self::bin_search(&self.array, old_alimit, real);
1337                self.alimit = boundary;
1338                return boundary as u64;
1339            }
1340        }
1341        let limit = self.real_asize();
1342        debug_assert!(
1343            limit == self.real_asize()
1344                && (limit == 0 || !matches!(self.array[(limit - 1) as usize], LuaValue::Nil))
1345        );
1346        let next_key = (limit as i64).saturating_add(1);
1347        let next_slot = self.get_int_slot(next_key);
1348        let next_empty = matches!(next_slot, TableSlotRef::Absent)
1349            || matches!(self.slot_value(next_slot), LuaValue::Nil);
1350        if self.is_dummy() || next_empty {
1351            return limit as u64;
1352        }
1353        self.hash_search(limit as u64)
1354    }
1355}
1356
1357// ── LuaTable (outer handle) ────────────────────────────────────────────────────
1358
1359/// A Lua table: hybrid array + hash map.
1360///
1361/// All public methods take `&self` so the type works through
1362/// `GcRef<LuaTable>` (which only dereferences to a shared borrow).
1363/// Mutations are routed through an internal [`RefCell`].
1364#[derive(Debug)]
1365pub struct LuaTable {
1366    inner: RefCell<TableInner>,
1367    metatable: RefCell<Option<GcRef<LuaTable>>>,
1368    has_metatable: Cell<bool>,
1369    weak_mode: Cell<u8>,
1370}
1371
1372impl std::fmt::Debug for TableInner {
1373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1374        f.debug_struct("TableInner")
1375            .field("alimit", &self.alimit)
1376            .field("array_len", &self.array.len())
1377            .field("node_len", &self.node.len())
1378            .finish()
1379    }
1380}
1381
1382impl Default for LuaTable {
1383    fn default() -> Self {
1384        LuaTable {
1385            inner: RefCell::new(TableInner::new()),
1386            metatable: RefCell::new(None),
1387            has_metatable: Cell::new(false),
1388            weak_mode: Cell::new(0),
1389        }
1390    }
1391}
1392
1393impl LuaTable {
1394    /// Construct an empty table. Used as a placeholder by callers that
1395    /// will populate it via the normal API.
1396    pub fn placeholder() -> Self {
1397        Self::default()
1398    }
1399
1400    /// Borrow inner storage for read access. Intended for advanced
1401    /// callers (e.g. the GC trace impl); prefer the typed methods.
1402    pub fn with_inner<R>(&self, f: impl FnOnce(&TableInner) -> R) -> R {
1403        f(&self.inner.borrow())
1404    }
1405
1406    /// Bytes of heap-allocated buffer backing this table's array and node
1407    /// parts, measured by *capacity* (the bytes actually reserved from the
1408    /// allocator, not just the populated prefix). Read-only; used by the GC
1409    /// pacer-accounting path to charge these buffers against the heap.
1410    pub fn buffer_bytes(&self) -> usize {
1411        let inner = self.inner.borrow();
1412        inner.array.capacity() * std::mem::size_of::<LuaValue>()
1413            + inner.node.capacity() * std::mem::size_of::<TableNode>()
1414    }
1415
1416    /// Read a key. Returns `LuaValue::Nil` if absent or if `k` is nil.
1417    /// Integer keys take the direct array-part fast path used by
1418    /// [`LuaTable::get_int`]; short-string keys take the analogous
1419    /// hash-chain fast path used by [`LuaTable::get_short_str`]; every
1420    /// other key shape falls through to the cold generic slot lookup.
1421    /// Marked `#[inline(always)]` so the dispatch folds into the
1422    /// caller (the hot `state::fast_get` / `state::table_get_with_tm`
1423    /// frames in the VM); profiling at #[inline] showed LLVM was still
1424    /// emitting a cross-crate function call here.
1425    #[inline(always)]
1426    pub fn get(&self, k: &LuaValue) -> LuaValue {
1427        let inner = self.inner.borrow();
1428        match k {
1429            LuaValue::Nil => LuaValue::Nil,
1430            LuaValue::Int(i) => inner.get_int_value(*i),
1431            LuaValue::Str(s) if s.is_short() => inner.get_str_value(s),
1432            _ => inner.get_generic_value(k),
1433        }
1434    }
1435
1436    /// Read by integer key. Hot path: callers like `state.fast_get_int`
1437    /// and `state.table_get_with_tm` dispatch here on every integer-key
1438    /// access in user code (`t[1]`, `OP_GETI`, ipairs loops, etc.). The
1439    /// array-part lookup folds into a single bounds-checked load,
1440    /// matching C's `luaH_getint`.
1441    #[inline(always)]
1442    pub fn get_int(&self, key: i64) -> LuaValue {
1443        let inner = self.inner.borrow();
1444        inner.get_int_value(key)
1445    }
1446
1447    /// Read by string key. Despite the name (kept for compatibility
1448    /// with the old API), this dispatches internally to either the
1449    /// short- or long-string path; passing a long string is safe. The
1450    /// short-string branch (the common case — all interned identifiers
1451    /// and most table-field keys are short) takes the folded hash-walk
1452    /// in [`TableInner::get_str_value`]; long strings still go through
1453    /// the slot indirection.
1454    #[inline(always)]
1455    pub fn get_short_str(&self, k: &GcRef<LuaString>) -> LuaValue {
1456        let inner = self.inner.borrow();
1457        if k.is_short() {
1458            inner.get_str_value(k)
1459        } else {
1460            let slot = inner.get_str_slot(k);
1461            inner.slot_value(slot)
1462        }
1463    }
1464
1465    /// Read by raw byte-string key. Linear scan over the hash part —
1466    /// rarely-used helper for callers that don't have a `GcRef<LuaString>`
1467    /// handle.
1468    pub fn get_str_bytes(&self, key_bytes: &[u8]) -> LuaValue {
1469        let mut found = LuaValue::Nil;
1470        self.for_each_entry(|k, v| {
1471            if !matches!(found, LuaValue::Nil) {
1472                return;
1473            }
1474            if let LuaValue::Str(s) = k {
1475                if s.as_bytes() == key_bytes {
1476                    found = v.clone();
1477                }
1478            }
1479        });
1480        found
1481    }
1482
1483    /// Raw set without metamethod dispatch. Nil keys are an error;
1484    /// NaN-float keys are an error. Setting `v == Nil` clears the slot.
1485    pub fn raw_set(&self, k: LuaValue, v: LuaValue) {
1486        if matches!(k, LuaValue::Nil) {
1487            return;
1488        }
1489        if let LuaValue::Float(f) = &k {
1490            if f.is_nan() {
1491                return;
1492            }
1493        }
1494        let mut inner = self.inner.borrow_mut();
1495        let _ = inner.set(&k, v);
1496    }
1497
1498    /// Raw set with explicit error returns; preferred path used by
1499    /// `LuaTableRefExt::raw_set` in `lua-vm`. Integer keys (and floats
1500    /// that are exact integers) take the same direct array-part fast
1501    /// path used by [`LuaTable::try_raw_set_int`]; other key shapes
1502    /// fall through to the generic slot lookup.
1503    #[inline]
1504    pub fn try_raw_set(&self, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1505        match &k {
1506            LuaValue::Nil => Err(LuaError::runtime(format_args!("table index is nil"))),
1507            LuaValue::Float(f) if f.is_nan() => {
1508                Err(LuaError::runtime(format_args!("table index is NaN")))
1509            }
1510            LuaValue::Int(i) => {
1511                let key = *i;
1512                let mut inner = self.inner.borrow_mut();
1513                inner.try_raw_set_int_fast(key, v)
1514            }
1515            LuaValue::Float(f) => {
1516                let f = *f;
1517                let k_int = f as i64;
1518                if k_int as f64 == f {
1519                    let mut inner = self.inner.borrow_mut();
1520                    inner.try_raw_set_int_fast(k_int, v)
1521                } else {
1522                    self.try_raw_set_generic(k, v)
1523                }
1524            }
1525            _ => self.try_raw_set_generic(k, v),
1526        }
1527    }
1528
1529    /// Update an existing short-string slot without routing through the
1530    /// generic cold setter. Returns the value to the caller when the key is
1531    /// absent so the insertion/rehash path can account buffer growth.
1532    #[inline(always)]
1533    pub fn try_update_short_str(&self, k: &GcRef<LuaString>, v: LuaValue) -> Result<(), LuaValue> {
1534        if !k.is_short() {
1535            return Err(v);
1536        }
1537        let mut inner = self.inner.borrow_mut();
1538        inner.try_update_short_str(k, v)
1539    }
1540
1541    /// Update an existing integer slot without buffer accounting. This covers
1542    /// the stable `SETI` hot path; absent keys still fall back to the normal
1543    /// set path so array growth and hash insertion remain accounted.
1544    #[inline(always)]
1545    pub fn try_update_int(&self, k: i64, v: LuaValue) -> Result<(), LuaValue> {
1546        let mut inner = self.inner.borrow_mut();
1547        inner.try_update_int(k, v)
1548    }
1549
1550    /// Generic-key path for [`Self::try_raw_set`]. Split out so the
1551    /// integer fast path stays branch-light and inlineable.
1552    #[cold]
1553    #[inline(never)]
1554    fn try_raw_set_generic(&self, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1555        let mut inner = self.inner.borrow_mut();
1556        if inner.array.len() + inner.node.len() >= TOTAL_GROW_CAP
1557            && matches!(inner.get_slot(&k), TableSlotRef::Absent)
1558        {
1559            return Err(LuaError::Memory);
1560        }
1561        inner.set(&k, v)
1562    }
1563
1564    /// Raw set by integer key with explicit error returns. Routes the
1565    /// array-part fast path through [`TableInner::set_int_value`] — a
1566    /// single bounds-checked store with no intermediate
1567    /// [`TableSlotRef`] indirection — and only consults the
1568    /// `TOTAL_GROW_CAP` allocation guard when the key would create a
1569    /// new slot.
1570    #[inline]
1571    pub fn try_raw_set_int(&self, k: i64, v: LuaValue) -> Result<(), LuaError> {
1572        let mut inner = self.inner.borrow_mut();
1573        inner.try_raw_set_int_fast(k, v)
1574    }
1575
1576    /// Resize the table to new array and hash sizes (sizing hint from
1577    /// the bytecode's `OP_NEWTABLE`).
1578    pub fn resize(&self, new_asize: u32, new_hsize: u32) -> Result<(), LuaError> {
1579        let mut inner = self.inner.borrow_mut();
1580        inner.resize(new_asize, new_hsize)
1581    }
1582
1583    /// Number of array-part slots currently allocated. Cheap counter
1584    /// for sizing decisions; NOT the Lua `#t` length operator.
1585    pub fn array_len(&self) -> usize {
1586        self.inner.borrow().array.len()
1587    }
1588
1589    /// Total occupied slots (array + hash) — used for legacy
1590    /// `len()` callers; prefer `getn` for the Lua `#` operator.
1591    pub fn len(&self) -> usize {
1592        let inner = self.inner.borrow();
1593        let mut n = 0usize;
1594        for v in inner.array.iter() {
1595            if !matches!(v, LuaValue::Nil) {
1596                n += 1;
1597            }
1598        }
1599        for node in inner.node.iter() {
1600            if !matches!(node.value, LuaValue::Nil) {
1601                n += 1;
1602            }
1603        }
1604        n
1605    }
1606    pub fn is_empty(&self) -> bool {
1607        self.len() == 0
1608    }
1609
1610    /// `#t` boundary (C: `luaH_getn`). Mutates internal caching state.
1611    pub fn getn(&self) -> u64 {
1612        let mut inner = self.inner.borrow_mut();
1613        inner.getn()
1614    }
1615
1616    /// Returns true iff `k` resolves to a slot in this table (array or
1617    /// hash). Used by `next` to validate the resumption key.
1618    pub fn contains_key(&self, k: &LuaValue) -> bool {
1619        if matches!(k, LuaValue::Nil) {
1620            return false;
1621        }
1622        let inner = self.inner.borrow();
1623        let slot = inner.get_slot(k);
1624        !matches!(slot, TableSlotRef::Absent)
1625    }
1626
1627    pub fn metatable(&self) -> Option<GcRef<LuaTable>> {
1628        self.metatable.borrow().clone()
1629    }
1630
1631    #[inline(always)]
1632    pub fn has_metatable(&self) -> bool {
1633        self.has_metatable.get()
1634    }
1635
1636    /// Install a metatable. Inspects its `__mode` field eagerly so the
1637    /// GC trace impl can read [`weak_mode`] without re-entering the
1638    /// metatable RefCell.
1639    pub fn set_metatable(&self, mt: Option<GcRef<LuaTable>>) {
1640        let mode = mt.as_ref().map(|t| extract_weak_mode(t)).unwrap_or(0);
1641        self.weak_mode.set(mode);
1642        self.has_metatable.set(mt.is_some());
1643        *self.metatable.borrow_mut() = mt;
1644    }
1645
1646    pub fn weak_mode(&self) -> u8 {
1647        self.weak_mode.get()
1648    }
1649
1650    /// Implements Lua's `next(t, k)`.
1651    pub fn next_pair(&self, k: &LuaValue) -> Option<(LuaValue, LuaValue)> {
1652        let inner = self.inner.borrow();
1653        inner.next_pair(k).ok().flatten()
1654    }
1655
1656    /// Like [`next_pair`] but reports the `"invalid key to 'next'"`
1657    /// error when `k` is non-nil and not present.
1658    pub fn try_next_pair(&self, k: &LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
1659        let inner = self.inner.borrow();
1660        inner.next_pair(k)
1661    }
1662
1663    /// Walk every live (key, value) pair via the given closure.
1664    /// Used by the GC trace impl to avoid the overhead of repeatedly
1665    /// re-entering `find_index` from `next_pair`.
1666    pub fn for_each_entry(&self, mut f: impl FnMut(&LuaValue, &LuaValue)) {
1667        let inner = self.inner.borrow();
1668        for (i, v) in inner.array.iter().enumerate() {
1669            if !matches!(v, LuaValue::Nil) {
1670                let k = LuaValue::Int((i + 1) as i64);
1671                f(&k, v);
1672            }
1673        }
1674        for node in inner.node.iter() {
1675            if !matches!(node.value, LuaValue::Nil) {
1676                f(&node.key, &node.value);
1677            }
1678        }
1679    }
1680
1681    /// Drop weak entries whose weakly-tracked target is unreachable,
1682    /// and return the list of values whose strings must still be
1683    /// marked by the caller.
1684    pub fn prune_weak_dead(&self, is_reachable: &dyn Fn(usize) -> bool) -> Vec<LuaValue> {
1685        self.prune_weak_dead_with(is_reachable, is_reachable)
1686    }
1687
1688    /// Variant of [`Self::prune_weak_dead`] that allows key and value sides to
1689    /// use different liveness predicates. Lua keeps objects pending finalization
1690    /// visible as weak keys until their `__gc` runs, but clears them from weak
1691    /// values before the finalizer.
1692    pub fn prune_weak_dead_with(
1693        &self,
1694        is_key_reachable: &dyn Fn(usize) -> bool,
1695        is_value_reachable: &dyn Fn(usize) -> bool,
1696    ) -> Vec<LuaValue> {
1697        self.prune_weak_dead_with_value(
1698            &|v| collectable_identity(v).map_or(true, is_key_reachable),
1699            &|v| collectable_identity(v).map_or(true, is_value_reachable),
1700        )
1701    }
1702
1703    /// Value-aware weak cleanup. Generational minor collection uses this to
1704    /// treat unmarked old values as live, because young sweep will not free
1705    /// them even when the minor marker skipped their subgraph.
1706    pub fn prune_weak_dead_with_value(
1707        &self,
1708        is_key_reachable: &dyn Fn(&LuaValue) -> bool,
1709        is_value_reachable: &dyn Fn(&LuaValue) -> bool,
1710    ) -> Vec<LuaValue> {
1711        let mode = self.weak_mode.get();
1712        if mode == 0 {
1713            return Vec::new();
1714        }
1715        let weak_k = (mode & WEAK_KEYS) != 0;
1716        let weak_v = (mode & WEAK_VALUES) != 0;
1717        let mut to_mark: Vec<LuaValue> = Vec::new();
1718        let mut inner = self.inner.borrow_mut();
1719        for i in 0..inner.array.len() {
1720            let v = inner.array[i].clone();
1721            if matches!(v, LuaValue::Nil) {
1722                continue;
1723            }
1724            if weak_v && value_is_dead_collectable(&v, is_value_reachable) {
1725                inner.array[i] = LuaValue::Nil;
1726                continue;
1727            }
1728            if weak_v {
1729                if matches!(v, LuaValue::Str(_)) {
1730                    to_mark.push(v);
1731                }
1732            }
1733        }
1734        let mut i = 0;
1735        while i < inner.node.len() {
1736            let v = inner.node[i].value.clone();
1737            if matches!(v, LuaValue::Nil) {
1738                i += 1;
1739                continue;
1740            }
1741            let k = inner.node[i].key.clone();
1742            if weak_v && value_is_dead_collectable(&v, is_value_reachable) {
1743                inner.clear_dead_hash_node(i);
1744                continue;
1745            }
1746            if weak_k && value_is_dead_collectable(&k, is_key_reachable) {
1747                inner.clear_dead_hash_node(i);
1748                continue;
1749            }
1750            if weak_k {
1751                if matches!(k, LuaValue::Str(_)) {
1752                    to_mark.push(k);
1753                }
1754            }
1755            if weak_v {
1756                if matches!(v, LuaValue::Str(_)) {
1757                    to_mark.push(v);
1758                }
1759            }
1760            i += 1;
1761        }
1762        to_mark
1763    }
1764
1765    /// Ephemeron-convergence helper for pure `__mode = "k"` tables.
1766    pub fn ephemeron_values_to_mark(&self, is_reachable: &dyn Fn(usize) -> bool) -> Vec<LuaValue> {
1767        self.ephemeron_values_to_mark_with_value(&|v| {
1768            collectable_identity(v).map_or(true, is_reachable)
1769        })
1770    }
1771
1772    /// Value-aware ephemeron helper for minor collections, where unmarked old
1773    /// keys are still live.
1774    pub fn ephemeron_values_to_mark_with_value(
1775        &self,
1776        is_reachable: &dyn Fn(&LuaValue) -> bool,
1777    ) -> Vec<LuaValue> {
1778        let mode = self.weak_mode.get();
1779        if (mode & WEAK_KEYS) == 0 || (mode & WEAK_VALUES) != 0 {
1780            return Vec::new();
1781        }
1782        let inner = self.inner.borrow();
1783        let mut out = Vec::new();
1784        for node in inner.node.iter() {
1785            if matches!(node.value, LuaValue::Nil) {
1786                continue;
1787            }
1788            if !value_is_dead_collectable(&node.key, is_reachable) {
1789                out.push(node.value.clone());
1790            }
1791        }
1792        for (i, v) in inner.array.iter().enumerate() {
1793            if matches!(v, LuaValue::Nil) {
1794                continue;
1795            }
1796            let k = LuaValue::Int((i + 1) as i64);
1797            if !value_is_dead_collectable(&k, is_reachable) {
1798                out.push(v.clone());
1799            }
1800        }
1801        out
1802    }
1803}
1804
1805// ── Free helpers ──────────────────────────────────────────────────────────────
1806
1807/// True iff `v` is a collectable non-string LuaValue whose target was
1808/// unreached during the mark phase. Strings are explicitly excluded.
1809fn value_is_dead_collectable(v: &LuaValue, is_reachable: &dyn Fn(&LuaValue) -> bool) -> bool {
1810    collectable_identity(v).is_some() && !is_reachable(v)
1811}
1812
1813fn collectable_identity(v: &LuaValue) -> Option<usize> {
1814    match v {
1815        LuaValue::Table(t) => Some(t.identity()),
1816        LuaValue::UserData(u) => Some(u.identity()),
1817        LuaValue::Thread(th) => Some(th.identity()),
1818        LuaValue::Function(c) => match c {
1819            LuaClosure::Lua(x) => Some(x.identity()),
1820            LuaClosure::C(x) => Some(x.identity()),
1821            LuaClosure::LightC(_) => None,
1822        },
1823        LuaValue::Str(_)
1824        | LuaValue::Nil
1825        | LuaValue::Bool(_)
1826        | LuaValue::Int(_)
1827        | LuaValue::Float(_)
1828        | LuaValue::LightUserData(_) => None,
1829    }
1830}
1831
1832/// Inspect a metatable's `__mode` field and produce the corresponding
1833/// `WEAK_KEYS | WEAK_VALUES` bitmask. Returns 0 when no `__mode` is
1834/// set or it is not a string.
1835fn extract_weak_mode(mt: &LuaTable) -> u8 {
1836    let inner = mt.inner.borrow();
1837    for node in inner.node.iter() {
1838        if let LuaValue::Str(ks) = &node.key {
1839            if ks.as_bytes() == b"__mode" {
1840                if let LuaValue::Str(vs) = &node.value {
1841                    let bytes = vs.as_bytes();
1842                    let mut mode = 0u8;
1843                    if bytes.iter().any(|b| *b == b'k') {
1844                        mode |= WEAK_KEYS;
1845                    }
1846                    if bytes.iter().any(|b| *b == b'v') {
1847                        mode |= WEAK_VALUES;
1848                    }
1849                    return mode;
1850                }
1851                return 0;
1852            }
1853        }
1854    }
1855    0
1856}
1857
1858// ──────────────────────────────────────────────────────────────────────────────
1859// PORT STATUS
1860//   source:        src/ltable.c (~995 lines, 28 functions), src/ltable.h
1861//   target_crate:  lua-types
1862//   confidence:    high
1863//   todos:         0
1864//   port_notes:    0
1865//   unsafe_blocks: 0
1866//   notes:         Canonical LuaTable: hybrid array + hash. Mirrors C's Table
1867//                  struct (flags, lsizenode, alimit, array, node, lastfree) with
1868//                  Vec<LuaValue> + Vec<TableNode> in place of raw C pointers, and
1869//                  Option<usize> indexing in place of Node*. The luaH_getn
1870//                  boundary search + alimit-aware integer-key fast path are
1871//                  ported faithfully (see getn() and get_int_slot()). The
1872//                  integer-key read path also exposes get_int_value, which
1873//                  mirrors C's luaH_getint by returning the array slot directly
1874//                  in one bounds-checked load (no TableSlotRef indirection) and
1875//                  splitting the rare alimit-aliased / hash-part path into a
1876//                  cold helper. The short-string read path mirrors that shape
1877//                  via get_str_value (single hash-chain walk, no TableSlotRef
1878//                  round-trip); LuaTable::get dispatches on integer/short-string
1879//                  keys inline and routes everything else through a #[cold]
1880//                  get_generic_value, matching C's luaH_get fast-path structure.
1881//                  LuaTable::get / get_int / get_short_str are #[inline(always)]
1882//                  so the dispatch folds into the cross-crate VM hot frames
1883//                  (state::fast_get / state::table_get_with_tm). Weak-table mode
1884//                  flags + the prune_weak_dead / ephemeron_values_to_mark
1885//                  helpers integrate with the lua-gc Trace impl.
1886// ──────────────────────────────────────────────────────────────────────────────