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