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    fn find_index(&self, key: &LuaValue, asize: u32) -> Result<u32, LuaError> {
578        if matches!(key, LuaValue::Nil) {
579            return Ok(0);
580        }
581        let i = if let LuaValue::Int(k) = key {
582            Self::array_index(*k)
583        } else {
584            0
585        };
586        if i.wrapping_sub(1) < asize {
587            return Ok(i);
588        }
589        let slot = self.get_generic_slot_deadok(key);
590        match slot {
591            TableSlotRef::Absent => Err(LuaError::runtime(format_args!("invalid key to 'next'"))),
592            TableSlotRef::Hash(node_idx) => Ok((node_idx as u32 + 1) + asize),
593            TableSlotRef::Array(_) => unreachable!("getgeneric returned Array slot"),
594        }
595    }
596
597    /// Iteration step: given a key (`Nil` for first call), return the
598    /// next `(key, value)` pair in C-Lua's array-then-hash order.
599    fn next_pair(&self, key: &LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
600        let asize = self.real_asize();
601        let i = self.find_index(key, asize)?;
602        let mut i = i as usize;
603        while i < asize as usize {
604            if !matches!(self.array[i], LuaValue::Nil) {
605                return Ok(Some((LuaValue::Int((i + 1) as i64), self.array[i].clone())));
606            }
607            i += 1;
608        }
609        let mut hi = i.saturating_sub(asize as usize);
610        while hi < self.node.len() {
611            if !matches!(self.node[hi].value, LuaValue::Nil) {
612                return Ok(Some((
613                    self.node[hi].key_value(),
614                    self.node[hi].value.clone(),
615                )));
616            }
617            hi += 1;
618        }
619        Ok(None)
620    }
621
622    // ── Rehash helpers ─────────────────────────────────────────────────────
623
624    fn compute_sizes(nums: &[u32], pna: &mut u32) -> u32 {
625        let mut twotoi: u32 = 1;
626        let mut a: u32 = 0;
627        let mut na: u32 = 0;
628        let mut optimal: u32 = 0;
629        for i in 0..nums.len() {
630            if twotoi == 0 || *pna <= twotoi / 2 {
631                break;
632            }
633            a += nums[i];
634            if a > twotoi / 2 {
635                optimal = twotoi;
636                na = a;
637            }
638            twotoi = twotoi.wrapping_mul(2);
639        }
640        debug_assert!(optimal == 0 || optimal / 2 < na && na <= optimal);
641        *pna = na;
642        optimal
643    }
644
645    fn count_int(key: i64, nums: &mut [u32]) -> bool {
646        let k = Self::array_index(key);
647        if k != 0 {
648            nums[ceil_log2(k) as usize] += 1;
649            true
650        } else {
651            false
652        }
653    }
654
655    fn num_use_array(&self, nums: &mut [u32]) -> u32 {
656        debug_assert!(
657            self.is_real_asize(),
658            "numusearray: alimit must be real size"
659        );
660        let asize = self.alimit as usize;
661        let mut ause: u32 = 0;
662        let mut i: usize = 1;
663        let mut ttlg: usize = 1;
664        for lg in 0..=(MAXABITS as usize) {
665            let mut lc: u32 = 0;
666            let lim = if ttlg > asize { asize } else { ttlg };
667            if i > lim {
668                break;
669            }
670            while i <= lim {
671                if !matches!(self.array[i - 1], LuaValue::Nil) {
672                    lc += 1;
673                }
674                i += 1;
675            }
676            nums[lg] += lc;
677            ause += lc;
678            ttlg = ttlg.saturating_mul(2);
679        }
680        ause
681    }
682
683    fn num_use_hash(&self, nums: &mut [u32], pna: &mut u32) -> i32 {
684        let mut totaluse: i32 = 0;
685        let mut ause: u32 = 0;
686        let mut i = self.node.len();
687        while i > 0 {
688            i -= 1;
689            let n = &self.node[i];
690            if !matches!(n.value, LuaValue::Nil) {
691                if n.key_is_int() {
692                    if Self::count_int(n.key_int(), nums) {
693                        ause += 1;
694                    }
695                }
696                totaluse += 1;
697            }
698        }
699        *pna += ause;
700        totaluse
701    }
702
703    /// Rebuild the array part to exactly `new_size` slots, preserving the
704    /// live prefix and `Nil`-filling any growth tail.
705    ///
706    /// This is the array-part analogue of [`Self::set_node_vector`]: with a
707    /// boxed slice there is no in-place `truncate`/`resize_with`, so every
708    /// size change allocates a fresh box, moves the survivors (the prefix
709    /// `[0, min(old_len, new_size))`) into it by value — no clone — and
710    /// swaps it in. On grow the appended tail is `Nil`; on shrink the
711    /// dropped suffix is freed with the old box. The caller owns the
712    /// `alimit` bookkeeping and any re-insertion of displaced keys, exactly
713    /// as with the prior `Vec` code.
714    fn set_array_size(&mut self, new_size: usize) {
715        let old = std::mem::take(&mut self.array);
716        let keep = old.len().min(new_size);
717        let mut survivors = old.into_vec();
718        survivors.truncate(keep);
719        survivors.reserve_exact(new_size - keep);
720        survivors.resize_with(new_size, || LuaValue::Nil);
721        self.array = survivors.into_boxed_slice();
722    }
723
724    fn set_node_vector(&mut self, size: u32) -> Result<(), LuaError> {
725        if size == 0 {
726            self.node = Box::default();
727            self.lsizenode = 0;
728            self.lastfree = NO_LASTFREE;
729        } else {
730            let lsize = ceil_log2(size);
731            if lsize as u32 > MAXHBITS || (1u32 << lsize) > MAXHSIZE {
732                return Err(LuaError::runtime(format_args!("table overflow")));
733            }
734            let actual_size = 1u32 << lsize;
735            let nodes: Vec<TableNode> = (0..actual_size).map(|_| TableNode::empty()).collect();
736            self.node = nodes.into_boxed_slice();
737            self.lsizenode = lsize as u8;
738            self.lastfree = actual_size;
739        }
740        Ok(())
741    }
742
743    fn reinsert(&mut self, old_nodes: Vec<(LuaValue, LuaValue)>) -> Result<(), LuaError> {
744        for (k, v) in old_nodes {
745            self.set(&k, v)?;
746        }
747        Ok(())
748    }
749
750    /// Resize the table to new array and hash sizes.
751    fn resize(&mut self, new_asize: u32, nhsize: u32) -> Result<(), LuaError> {
752        let old_asize = self.set_limit_to_size();
753
754        let (mut new_hash_node, mut new_hash_lsize, mut new_hash_lastfree) = {
755            let mut tmp = TableInner::new();
756            tmp.set_node_vector(nhsize)?;
757            (tmp.node, tmp.lsizenode, tmp.lastfree)
758        };
759
760        if new_asize < old_asize {
761            let migrate_end = (old_asize as usize).min(self.array.len());
762            let detached: Vec<(i64, LuaValue)> = ((new_asize as usize)..migrate_end)
763                .filter(|&i| !matches!(self.array[i], LuaValue::Nil))
764                .map(|i| ((i + 1) as i64, self.array[i].clone()))
765                .collect();
766            self.set_array_size(new_asize as usize);
767            self.alimit = new_asize;
768
769            std::mem::swap(&mut self.node, &mut new_hash_node);
770            std::mem::swap(&mut self.lsizenode, &mut new_hash_lsize);
771            std::mem::swap(&mut self.lastfree, &mut new_hash_lastfree);
772
773            for (key, v) in detached {
774                self.set_int(key, v)?;
775            }
776
777            self.alimit = old_asize;
778            std::mem::swap(&mut self.node, &mut new_hash_node);
779            std::mem::swap(&mut self.lsizenode, &mut new_hash_lsize);
780            std::mem::swap(&mut self.lastfree, &mut new_hash_lastfree);
781        }
782
783        self.set_array_size(new_asize as usize);
784
785        std::mem::swap(&mut self.node, &mut new_hash_node);
786        std::mem::swap(&mut self.lsizenode, &mut new_hash_lsize);
787        std::mem::swap(&mut self.lastfree, &mut new_hash_lastfree);
788        self.alimit = new_asize;
789
790        let old_hash_entries: Vec<(LuaValue, LuaValue)> = new_hash_node
791            .iter()
792            .filter(|n| !matches!(n.value, LuaValue::Nil))
793            .map(|n| (n.key_value(), n.value.clone()))
794            .collect();
795        drop(new_hash_node);
796        self.reinsert(old_hash_entries)?;
797
798        Ok(())
799    }
800
801    fn rehash(&mut self, extra_key: &LuaValue) -> Result<(), LuaError> {
802        let mut nums = [0u32; MAXABITS as usize + 1];
803        self.set_limit_to_size();
804
805        let na = self.num_use_array(&mut nums);
806        let mut na = na;
807        let mut totaluse = na as i32;
808
809        totaluse += self.num_use_hash(&mut nums, &mut na);
810
811        if let LuaValue::Int(ek) = extra_key {
812            if Self::count_int(*ek, &mut nums) {
813                na += 1;
814            }
815        }
816        totaluse += 1;
817
818        let asize = Self::compute_sizes(&nums, &mut na);
819
820        let nh = (totaluse - na as i32).max(0) as u32;
821        self.resize(asize, nh)
822    }
823
824    fn get_free_pos(&mut self) -> Option<usize> {
825        if self.is_dummy() {
826            return None;
827        }
828        loop {
829            if self.lastfree == NO_LASTFREE {
830                return None;
831            }
832            if self.lastfree == 0 {
833                self.lastfree = NO_LASTFREE;
834                return None;
835            }
836            let idx = (self.lastfree - 1) as usize;
837            self.lastfree = idx as u32;
838            if self.node[idx].key_is_nil() {
839                return Some(idx);
840            }
841        }
842    }
843
844    fn find_chain_predecessor(&self, idx: usize) -> Option<usize> {
845        self.node
846            .iter()
847            .enumerate()
848            .find(|(prev, node)| {
849                node.next != 0 && (*prev as isize + node.next as isize) == idx as isize
850            })
851            .map(|(prev, _)| prev)
852    }
853
854    fn clear_node(&mut self, idx: usize) {
855        self.node[idx].key = LuaValue::Nil;
856        self.node[idx].value = LuaValue::Nil;
857        self.node[idx].next = 0;
858    }
859
860    fn remove_hash_node(&mut self, idx: usize) {
861        if let Some(prev) = self.find_chain_predecessor(idx) {
862            let next = self.node[idx].next;
863            self.node[prev].next = if next == 0 {
864                0
865            } else {
866                let target = idx as isize + next as isize;
867                (target - prev as isize) as i32
868            };
869            self.clear_node(idx);
870            return;
871        }
872
873        let next = self.node[idx].next;
874        if next == 0 {
875            self.clear_node(idx);
876            return;
877        }
878
879        let next_idx = (idx as isize + next as isize) as usize;
880        let moved_next = self.node[next_idx].next;
881        let moved_key = self.node[next_idx].key_value();
882        let moved_value = self.node[next_idx].value.clone();
883        self.node[idx].key = moved_key;
884        self.node[idx].value = moved_value;
885        self.node[idx].next = if moved_next == 0 {
886            0
887        } else {
888            let target = next_idx as isize + moved_next as isize;
889            (target - idx as isize) as i32
890        };
891        self.clear_node(next_idx);
892    }
893
894    fn clear_dead_hash_node(&mut self, idx: usize) {
895        self.remove_hash_node(idx);
896    }
897
898    fn new_key(&mut self, key: &LuaValue, value: LuaValue) -> Result<(), LuaError> {
899        if matches!(key, LuaValue::Nil) {
900            return Err(LuaError::runtime(format_args!("table index is nil")));
901        }
902        let normalised_key;
903        let key = if let LuaValue::Float(f) = key {
904            let f = *f;
905            if f.is_nan() {
906                return Err(LuaError::runtime(format_args!("table index is NaN")));
907            }
908            let k = f as i64;
909            if k as f64 == f {
910                normalised_key = LuaValue::Int(k);
911                &normalised_key
912            } else {
913                key
914            }
915        } else {
916            key
917        };
918
919        if matches!(value, LuaValue::Nil) {
920            return Ok(());
921        }
922
923        if self.is_dummy() && !matches!(key, LuaValue::Int(_)) {
924            self.set_node_vector(DUMMY_TABLE_INIT_HASH_NODES)?;
925            let mp = self.main_position(key);
926            self.node[mp].set_key(key);
927            self.node[mp].value = value;
928            return Ok(());
929        }
930
931        let mp = self.main_position(key);
932        let mp_occupied = self.is_dummy() || !matches!(self.node[mp].value, LuaValue::Nil);
933        if mp_occupied {
934            let f = self.get_free_pos();
935            let f = match f {
936                None => {
937                    self.rehash(key)?;
938                    return self.set(key, value);
939                }
940                Some(idx) => idx,
941            };
942
943            debug_assert!(!self.is_dummy());
944            let othern = self.main_position_from_node(mp);
945
946            if othern != mp {
947                let mut prev = othern;
948                let mut steps = 0usize;
949                while (prev as isize + self.node[prev].next as isize) as usize != mp {
950                    steps += 1;
951                    if steps > self.node.len() {
952                        panic!(
953                            "table hash chain invariant broken: node {} unreachable from main position {} \
954                             ({} nodes; usually a missing GC key barrier — see ltable.c:717 parity note)",
955                            mp,
956                            othern,
957                            self.node.len()
958                        );
959                    }
960                    prev = (prev as isize + self.node[prev].next as isize) as usize;
961                }
962                self.node[prev].next = (f as isize - prev as isize) as i32;
963                let mp_key = self.node[mp].key_value();
964                let mp_val = self.node[mp].value.clone();
965                let mp_next = self.node[mp].next;
966                self.node[f].key = mp_key;
967                self.node[f].value = mp_val;
968                if mp_next != 0 {
969                    self.node[f].next = mp_next + (mp as isize - f as isize) as i32;
970                    self.node[mp].next = 0;
971                } else {
972                    self.node[f].next = 0;
973                }
974                self.node[mp].value = LuaValue::Nil;
975            } else {
976                if self.node[mp].next != 0 {
977                    let target = (mp as isize + self.node[mp].next as isize) as usize;
978                    self.node[f].next = (target as isize - f as isize) as i32;
979                } else {
980                    debug_assert!(self.node[f].next == 0);
981                }
982                self.node[mp].next = (f as isize - mp as isize) as i32;
983                self.node[f].set_key(key);
984                debug_assert!(matches!(self.node[f].value, LuaValue::Nil));
985                self.node[f].value = value;
986                return Ok(());
987            }
988        }
989        self.node[mp].set_key(key);
990        debug_assert!(matches!(self.node[mp].value, LuaValue::Nil));
991        self.node[mp].value = value;
992        Ok(())
993    }
994
995    fn get_int_slot(&self, key: i64) -> TableSlotRef {
996        let alimit = self.alimit as u64;
997        let uk = key as u64;
998        if uk.wrapping_sub(1) < alimit {
999            return TableSlotRef::Array((key - 1) as usize);
1000        }
1001        if !self.is_real_asize() && alimit > 0 {
1002            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1003            if masked < alimit {
1004                return TableSlotRef::Array((key - 1) as usize);
1005            }
1006        }
1007        if self.is_dummy() {
1008            return TableSlotRef::Absent;
1009        }
1010        let mut n = self.hash_idx_for_int(key);
1011        loop {
1012            if self.node[n].key_is_int() && self.node[n].key_int() == key {
1013                return TableSlotRef::Hash(n);
1014            }
1015            let nx = self.node[n].next;
1016            if nx == 0 {
1017                break;
1018            }
1019            n = (n as isize + nx as isize) as usize;
1020        }
1021        TableSlotRef::Absent
1022    }
1023
1024    /// Read an integer key directly to a [`LuaValue`], mirroring C's
1025    /// `luaH_getint`. The array-part fast path returns the slot in a
1026    /// single bounds-checked load without constructing an intermediate
1027    /// [`TableSlotRef`] enum; only when the key falls through to the
1028    /// hash part do we walk the chain. Equivalent in observable
1029    /// behaviour to `slot_value(get_int_slot(key))`.
1030    #[inline]
1031    fn get_int_value(&self, key: i64) -> LuaValue {
1032        let alimit = self.alimit as u64;
1033        let uk = key as u64;
1034        if uk.wrapping_sub(1) < alimit {
1035            return self.array[(key - 1) as usize].clone();
1036        }
1037        self.get_int_value_cold(key)
1038    }
1039
1040    #[cold]
1041    #[inline(never)]
1042    fn get_int_value_cold(&self, key: i64) -> LuaValue {
1043        let alimit = self.alimit as u64;
1044        let uk = key as u64;
1045        if !self.is_real_asize() && alimit > 0 {
1046            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1047            if masked < alimit {
1048                return self.array[(key - 1) as usize].clone();
1049            }
1050        }
1051        if self.is_dummy() {
1052            return LuaValue::Nil;
1053        }
1054        let mut n = self.hash_idx_for_int(key);
1055        loop {
1056            if self.node[n].key_is_int() && self.node[n].key_int() == key {
1057                return self.node[n].value.clone();
1058            }
1059            let nx = self.node[n].next;
1060            if nx == 0 {
1061                break;
1062            }
1063            n = (n as isize + nx as isize) as usize;
1064        }
1065        LuaValue::Nil
1066    }
1067
1068    #[inline(always)]
1069    fn get_short_str_slot(&self, key: &GcRef<LuaString>) -> TableSlotRef {
1070        debug_assert!(key.is_short());
1071        if self.is_dummy() {
1072            return TableSlotRef::Absent;
1073        }
1074        let mut n = self.hashpow2_idx(key.hash());
1075        loop {
1076            if self.node[n].key_is_short_str() {
1077                let ks = self.node[n].key_string();
1078                if lua_string_content_eq(ks, key) {
1079                    return TableSlotRef::Hash(n);
1080                }
1081            }
1082            let nx = self.node[n].next;
1083            if nx == 0 {
1084                return TableSlotRef::Absent;
1085            }
1086            n = (n as isize + nx as isize) as usize;
1087        }
1088    }
1089
1090    #[inline(always)]
1091    fn try_update_short_str(
1092        &mut self,
1093        key: &GcRef<LuaString>,
1094        value: LuaValue,
1095    ) -> Result<(), LuaValue> {
1096        debug_assert!(key.is_short());
1097        if self.is_dummy() {
1098            return Err(value);
1099        }
1100        let mut n = self.hashpow2_idx(key.hash());
1101        loop {
1102            if self.node[n].key_is_short_str() {
1103                let ks = self.node[n].key_string();
1104                if lua_string_content_eq(ks, key) {
1105                    self.node[n].value = value;
1106                    return Ok(());
1107                }
1108            }
1109            let nx = self.node[n].next;
1110            if nx == 0 {
1111                return Err(value);
1112            }
1113            n = (n as isize + nx as isize) as usize;
1114        }
1115    }
1116
1117    #[inline(always)]
1118    fn try_update_int(&mut self, key: i64, value: LuaValue) -> Result<(), LuaValue> {
1119        let alimit = self.alimit as u64;
1120        let uk = key as u64;
1121        if uk.wrapping_sub(1) < alimit {
1122            self.array[(key - 1) as usize] = value;
1123            return Ok(());
1124        }
1125        if !self.is_real_asize() && alimit > 0 {
1126            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1127            if masked < alimit {
1128                self.array[(key - 1) as usize] = value;
1129                return Ok(());
1130            }
1131        }
1132        if !self.is_dummy() {
1133            let mut n = self.hash_idx_for_int(key);
1134            loop {
1135                if self.node[n].key_is_int() && self.node[n].key_int() == key {
1136                    self.node[n].value = value;
1137                    return Ok(());
1138                }
1139                let nx = self.node[n].next;
1140                if nx == 0 {
1141                    break;
1142                }
1143                n = (n as isize + nx as isize) as usize;
1144            }
1145        }
1146        Err(value)
1147    }
1148
1149    /// Read a short-string key directly to a [`LuaValue`], mirroring the
1150    /// shape of [`Self::get_int_value`]: a single hash-chain walk that
1151    /// produces the slot's value without constructing an intermediate
1152    /// [`TableSlotRef`] enum. Short strings are interned, so pointer
1153    /// equality wins almost every comparison; the byte-equality fallback
1154    /// handles the rare cross-interning-table path. Callers must ensure
1155    /// `key.is_short()` before dispatching here.
1156    #[inline]
1157    fn get_str_value(&self, key: &GcRef<LuaString>) -> LuaValue {
1158        debug_assert!(key.is_short());
1159        if self.is_dummy() {
1160            return LuaValue::Nil;
1161        }
1162        let mut n = self.hashpow2_idx(key.hash());
1163        loop {
1164            if self.node[n].key_is_short_str() {
1165                let ks = self.node[n].key_string();
1166                if lua_string_content_eq(ks, key) {
1167                    return self.node[n].value.clone();
1168                }
1169            }
1170            let nx = self.node[n].next;
1171            if nx == 0 {
1172                return LuaValue::Nil;
1173            }
1174            n = (n as isize + nx as isize) as usize;
1175        }
1176    }
1177
1178    /// Cold fallback for keys that miss the integer- and short-string
1179    /// fast paths in [`LuaTable::get`] (long strings, booleans,
1180    /// non-integer floats, table / function keys, light userdata, …).
1181    /// Routes through the existing `get_slot` + `slot_value` pair.
1182    #[cold]
1183    #[inline(never)]
1184    fn get_generic_value(&self, key: &LuaValue) -> LuaValue {
1185        let slot = self.get_slot(key);
1186        self.slot_value(slot)
1187    }
1188
1189    fn get_str_slot(&self, key: &GcRef<LuaString>) -> TableSlotRef {
1190        if key.is_short() {
1191            self.get_short_str_slot(key)
1192        } else {
1193            let ko = LuaValue::Str(key.clone());
1194            self.get_generic_slot(&ko)
1195        }
1196    }
1197
1198    fn get_slot(&self, key: &LuaValue) -> TableSlotRef {
1199        match key {
1200            LuaValue::Str(s) if s.is_short() => self.get_short_str_slot(s),
1201            LuaValue::Int(i) => self.get_int_slot(*i),
1202            LuaValue::Nil => TableSlotRef::Absent,
1203            LuaValue::Float(f) => {
1204                let f = *f;
1205                let k = f as i64;
1206                if k as f64 == f {
1207                    self.get_int_slot(k)
1208                } else {
1209                    self.get_generic_slot(key)
1210                }
1211            }
1212            _ => self.get_generic_slot(key),
1213        }
1214    }
1215
1216    fn slot_value(&self, slot: TableSlotRef) -> LuaValue {
1217        match slot {
1218            TableSlotRef::Array(i) => self.array[i].clone(),
1219            TableSlotRef::Hash(i) => self.node[i].value.clone(),
1220            TableSlotRef::Absent => LuaValue::Nil,
1221        }
1222    }
1223
1224    fn finish_set(
1225        &mut self,
1226        key: &LuaValue,
1227        slot: TableSlotRef,
1228        value: LuaValue,
1229    ) -> Result<(), LuaError> {
1230        match slot {
1231            TableSlotRef::Absent => self.new_key(key, value),
1232            TableSlotRef::Array(i) => {
1233                self.array[i] = value;
1234                Ok(())
1235            }
1236            TableSlotRef::Hash(i) => {
1237                self.node[i].value = value;
1238                Ok(())
1239            }
1240        }
1241    }
1242
1243    fn set(&mut self, key: &LuaValue, value: LuaValue) -> Result<(), LuaError> {
1244        let slot = self.get_slot(key);
1245        self.finish_set(key, slot, value)
1246    }
1247
1248    /// Set by integer key. May grow the array part up to
1249    /// [`ARRAY_GROW_CAP`] for keys just past `alimit` to amortise the
1250    /// common `t[#t+1] = v` pattern.
1251    fn set_int(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1252        let slot = self.get_int_slot(key);
1253        if matches!(slot, TableSlotRef::Absent) {
1254            if key > 0 && (key as u64) <= ARRAY_GROW_CAP as u64 {
1255                let cur = self.alimit as i64;
1256                if key == cur + 1 && !matches!(value, LuaValue::Nil) {
1257                    let new_size = (key as u32).next_power_of_two().max(4);
1258                    let capped = new_size.min(ARRAY_GROW_CAP);
1259                    if capped > self.alimit {
1260                        let nsize = self.alloc_sizenode();
1261                        self.resize(capped, nsize)?;
1262                        let new_slot = self.get_int_slot(key);
1263                        return self.finish_set(&LuaValue::Int(key), new_slot, value);
1264                    }
1265                }
1266            }
1267        }
1268        match slot {
1269            TableSlotRef::Absent => {
1270                let k = LuaValue::Int(key);
1271                self.new_key(&k, value)
1272            }
1273            TableSlotRef::Array(i) => {
1274                self.array[i] = value;
1275                Ok(())
1276            }
1277            TableSlotRef::Hash(i) => {
1278                self.node[i].value = value;
1279                Ok(())
1280            }
1281        }
1282    }
1283
1284    /// Integer-key entry used by [`LuaTable::try_raw_set`] /
1285    /// [`LuaTable::try_raw_set_int`]. The array fast path writes
1286    /// directly into a slot that is by definition already allocated
1287    /// (it lives inside the `Vec` at offset `key-1 < alimit`), so the
1288    /// `TOTAL_GROW_CAP` guard cannot apply. Only the cold path can
1289    /// allocate; the guard runs there.
1290    #[inline]
1291    fn try_raw_set_int_fast(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1292        let alimit = self.alimit as u64;
1293        let uk = key as u64;
1294        if uk.wrapping_sub(1) < alimit {
1295            self.array[(key - 1) as usize] = value;
1296            return Ok(());
1297        }
1298        self.try_raw_set_int_cold(key, value)
1299    }
1300
1301    #[cold]
1302    #[inline(never)]
1303    fn try_raw_set_int_cold(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1304        if self.array.len() + self.node.len() >= TOTAL_GROW_CAP
1305            && matches!(self.get_int_slot(key), TableSlotRef::Absent)
1306        {
1307            return Err(LuaError::Memory);
1308        }
1309        self.set_int_value_cold(key, value)
1310    }
1311
1312    /// Cold fallback for [`Self::set_int_value`]: handles the
1313    /// alimit-aliased slot (non-real-`asize` tables), hash-part lookup
1314    /// + in-place store, the array-grow-on-`#t+1` heuristic, and
1315    /// `new_key` insertion. Split into a `#[cold] #[inline(never)]`
1316    /// helper so LLVM lays out the array fast path as straight-line
1317    /// code in the inlined caller.
1318    #[cold]
1319    #[inline(never)]
1320    fn set_int_value_cold(&mut self, key: i64, value: LuaValue) -> Result<(), LuaError> {
1321        let alimit = self.alimit as u64;
1322        let uk = key as u64;
1323        if !self.is_real_asize() && alimit > 0 {
1324            let masked = (uk.wrapping_sub(1)) & !(alimit.wrapping_sub(1));
1325            if masked < alimit {
1326                self.array[(key - 1) as usize] = value;
1327                return Ok(());
1328            }
1329        }
1330        if !self.is_dummy() {
1331            let mut n = self.hash_idx_for_int(key);
1332            loop {
1333                if self.node[n].key_is_int() && self.node[n].key_int() == key {
1334                    self.node[n].value = value;
1335                    return Ok(());
1336                }
1337                let nx = self.node[n].next;
1338                if nx == 0 {
1339                    break;
1340                }
1341                n = (n as isize + nx as isize) as usize;
1342            }
1343        }
1344        if key > 0 && (key as u64) <= ARRAY_GROW_CAP as u64 {
1345            let cur = self.alimit as i64;
1346            if key == cur + 1 && !matches!(value, LuaValue::Nil) {
1347                let new_size = (key as u32).next_power_of_two().max(4);
1348                let capped = new_size.min(ARRAY_GROW_CAP);
1349                if capped > self.alimit {
1350                    let nsize = self.alloc_sizenode();
1351                    self.resize(capped, nsize)?;
1352                    let new_slot = self.get_int_slot(key);
1353                    return self.finish_set(&LuaValue::Int(key), new_slot, value);
1354                }
1355            }
1356        }
1357        let k = LuaValue::Int(key);
1358        self.new_key(&k, value)
1359    }
1360
1361    // ── boundary search ────────────────────────────────────────────────────
1362
1363    fn hash_search(&self, mut j: u64) -> u64 {
1364        let mut i: u64;
1365        if j == 0 {
1366            j = 1;
1367        }
1368        loop {
1369            i = j;
1370            if j <= (i64::MAX as u64) / 2 {
1371                j *= 2;
1372            } else {
1373                j = i64::MAX as u64;
1374                let s = self.get_int_slot(j as i64);
1375                if matches!(s, TableSlotRef::Absent) || matches!(self.slot_value(s), LuaValue::Nil)
1376                {
1377                    break;
1378                } else {
1379                    return j;
1380                }
1381            }
1382            let s = self.get_int_slot(j as i64);
1383            if matches!(s, TableSlotRef::Absent) {
1384                break;
1385            }
1386            if matches!(self.slot_value(s), LuaValue::Nil) {
1387                break;
1388            }
1389        }
1390        while j - i > 1 {
1391            let m = i / 2 + j / 2;
1392            let s = self.get_int_slot(m as i64);
1393            let empty =
1394                matches!(s, TableSlotRef::Absent) || matches!(self.slot_value(s), LuaValue::Nil);
1395            if empty {
1396                j = m;
1397            } else {
1398                i = m;
1399            }
1400        }
1401        i
1402    }
1403
1404    fn bin_search(array: &[LuaValue], mut i: u32, mut j: u32) -> u32 {
1405        while j - i > 1 {
1406            let m = (i + j) / 2;
1407            if matches!(array[(m - 1) as usize], LuaValue::Nil) {
1408                j = m;
1409            } else {
1410                i = m;
1411            }
1412        }
1413        i
1414    }
1415
1416    /// Find a boundary `i` such that `t[i]` is present and `t[i+1]` is absent,
1417    /// or 0 if `t[1]` is absent. C: `luaH_getn`.
1418    fn getn(&mut self) -> u64 {
1419        let limit = self.alimit;
1420        if limit > 0 && matches!(self.array[(limit - 1) as usize], LuaValue::Nil) {
1421            if limit >= 2 && !matches!(self.array[(limit - 2) as usize], LuaValue::Nil) {
1422                if self.is_pow2_real_asize() && !Self::is_pow2(limit - 1) {
1423                    self.alimit = limit - 1;
1424                    self.flags.set_no_real_asize();
1425                }
1426                return (limit - 1) as u64;
1427            } else {
1428                let boundary = Self::bin_search(&self.array, 0, limit);
1429                if self.is_pow2_real_asize() && boundary > self.real_asize() / 2 {
1430                    self.alimit = boundary;
1431                    self.flags.set_no_real_asize();
1432                }
1433                return boundary as u64;
1434            }
1435        }
1436        if !self.limit_equals_asize() {
1437            if matches!(self.array[limit as usize], LuaValue::Nil) {
1438                return limit as u64;
1439            }
1440            let real = self.real_asize();
1441            if matches!(self.array[(real - 1) as usize], LuaValue::Nil) {
1442                let old_alimit = self.alimit;
1443                let boundary = Self::bin_search(&self.array, old_alimit, real);
1444                self.alimit = boundary;
1445                return boundary as u64;
1446            }
1447        }
1448        let limit = self.real_asize();
1449        debug_assert!(
1450            limit == self.real_asize()
1451                && (limit == 0 || !matches!(self.array[(limit - 1) as usize], LuaValue::Nil))
1452        );
1453        let next_key = (limit as i64).saturating_add(1);
1454        let next_slot = self.get_int_slot(next_key);
1455        let next_empty = matches!(next_slot, TableSlotRef::Absent)
1456            || matches!(self.slot_value(next_slot), LuaValue::Nil);
1457        if self.is_dummy() || next_empty {
1458            return limit as u64;
1459        }
1460        self.hash_search(limit as u64)
1461    }
1462}
1463
1464// ── LuaTable (outer handle) ────────────────────────────────────────────────────
1465
1466/// A Lua table: hybrid array + hash map.
1467///
1468/// All public methods take `&self` so the type works through
1469/// `GcRef<LuaTable>` (which only dereferences to a shared borrow).
1470/// Mutations are routed through an internal [`RefCell`].
1471#[derive(Debug)]
1472pub struct LuaTable {
1473    inner: RefCell<TableInner>,
1474    /// `Cell`, not `RefCell`: `GcRef` is `Copy`, so get/set need no borrow
1475    /// flag — 8 bytes instead of 16, and `has_metatable` is just an
1476    /// `is_some()` on the loaded value rather than a separate cached bool.
1477    metatable: Cell<Option<GcRef<LuaTable>>>,
1478    weak_mode: Cell<u8>,
1479}
1480
1481impl std::fmt::Debug for TableInner {
1482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1483        f.debug_struct("TableInner")
1484            .field("alimit", &self.alimit)
1485            .field("array_len", &self.array.len())
1486            .field("node_len", &self.node.len())
1487            .finish()
1488    }
1489}
1490
1491impl Default for LuaTable {
1492    fn default() -> Self {
1493        LuaTable {
1494            inner: RefCell::new(TableInner::new()),
1495            metatable: Cell::new(None),
1496            weak_mode: Cell::new(0),
1497        }
1498    }
1499}
1500
1501impl LuaTable {
1502    /// Construct an empty table. Used as a placeholder by callers that
1503    /// will populate it via the normal API.
1504    pub fn placeholder() -> Self {
1505        Self::default()
1506    }
1507
1508    /// Borrow inner storage for read access. Intended for advanced
1509    /// callers (e.g. the GC trace impl); prefer the typed methods.
1510    pub fn with_inner<R>(&self, f: impl FnOnce(&TableInner) -> R) -> R {
1511        f(&self.inner.borrow())
1512    }
1513
1514    /// Bytes of heap-allocated buffer backing this table's array and node
1515    /// parts. The array and node parts are boxed slices, so their length is
1516    /// exactly the number of slots reserved from the allocator (`cap == len`);
1517    /// `len()` is therefore the precise reserved-byte count. Read-only; used
1518    /// by the GC pacer-accounting path to charge these buffers against the
1519    /// heap.
1520    pub fn buffer_bytes(&self) -> usize {
1521        let inner = self.inner.borrow();
1522        inner.array.len() * std::mem::size_of::<LuaValue>()
1523            + inner.node.len() * std::mem::size_of::<TableNode>()
1524    }
1525
1526    /// Read a key. Returns `LuaValue::Nil` if absent or if `k` is nil.
1527    /// Integer keys take the direct array-part fast path used by
1528    /// [`LuaTable::get_int`]; short-string keys take the analogous
1529    /// hash-chain fast path used by [`LuaTable::get_short_str`]; every
1530    /// other key shape falls through to the cold generic slot lookup.
1531    /// Marked `#[inline(always)]` so the dispatch folds into the
1532    /// caller (the hot `state::fast_get` / `state::table_get_with_tm`
1533    /// frames in the VM); profiling at #[inline] showed LLVM was still
1534    /// emitting a cross-crate function call here.
1535    #[inline(always)]
1536    pub fn get(&self, k: &LuaValue) -> LuaValue {
1537        let inner = self.inner.borrow();
1538        match k {
1539            LuaValue::Nil => LuaValue::Nil,
1540            LuaValue::Int(i) => inner.get_int_value(*i),
1541            LuaValue::Str(s) if s.is_short() => inner.get_str_value(s),
1542            _ => inner.get_generic_value(k),
1543        }
1544    }
1545
1546    /// Read by integer key. Hot path: callers like `state.fast_get_int`
1547    /// and `state.table_get_with_tm` dispatch here on every integer-key
1548    /// access in user code (`t[1]`, `OP_GETI`, ipairs loops, etc.). The
1549    /// array-part lookup folds into a single bounds-checked load,
1550    /// matching C's `luaH_getint`.
1551    #[inline(always)]
1552    pub fn get_int(&self, key: i64) -> LuaValue {
1553        let inner = self.inner.borrow();
1554        inner.get_int_value(key)
1555    }
1556
1557    /// Read by string key. Despite the name (kept for compatibility
1558    /// with the old API), this dispatches internally to either the
1559    /// short- or long-string path; passing a long string is safe. The
1560    /// short-string branch (the common case — all interned identifiers
1561    /// and most table-field keys are short) takes the folded hash-walk
1562    /// in [`TableInner::get_str_value`]; long strings still go through
1563    /// the slot indirection.
1564    #[inline(always)]
1565    pub fn get_short_str(&self, k: &GcRef<LuaString>) -> LuaValue {
1566        let inner = self.inner.borrow();
1567        if k.is_short() {
1568            inner.get_str_value(k)
1569        } else {
1570            let slot = inner.get_str_slot(k);
1571            inner.slot_value(slot)
1572        }
1573    }
1574
1575    /// Read by raw byte-string key. Linear scan over the hash part —
1576    /// rarely-used helper for callers that don't have a `GcRef<LuaString>`
1577    /// handle.
1578    pub fn get_str_bytes(&self, key_bytes: &[u8]) -> LuaValue {
1579        let mut found = LuaValue::Nil;
1580        self.for_each_entry(|k, v| {
1581            if !matches!(found, LuaValue::Nil) {
1582                return;
1583            }
1584            if let LuaValue::Str(s) = k {
1585                if s.as_bytes() == key_bytes {
1586                    found = v.clone();
1587                }
1588            }
1589        });
1590        found
1591    }
1592
1593    /// Raw set without metamethod dispatch. Nil keys are an error;
1594    /// NaN-float keys are an error. Setting `v == Nil` clears the slot.
1595    pub fn raw_set(&self, k: LuaValue, v: LuaValue) {
1596        if matches!(k, LuaValue::Nil) {
1597            return;
1598        }
1599        if let LuaValue::Float(f) = &k {
1600            if f.is_nan() {
1601                return;
1602            }
1603        }
1604        let mut inner = self.inner.borrow_mut();
1605        let _ = inner.set(&k, v);
1606    }
1607
1608    /// Raw set with explicit error returns; preferred path used by
1609    /// `LuaTableRefExt::raw_set` in `lua-vm`. Integer keys (and floats
1610    /// that are exact integers) take the same direct array-part fast
1611    /// path used by [`LuaTable::try_raw_set_int`]; other key shapes
1612    /// fall through to the generic slot lookup.
1613    #[inline]
1614    pub fn try_raw_set(&self, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1615        match &k {
1616            LuaValue::Nil => Err(LuaError::runtime(format_args!("table index is nil"))),
1617            LuaValue::Float(f) if f.is_nan() => {
1618                Err(LuaError::runtime(format_args!("table index is NaN")))
1619            }
1620            LuaValue::Int(i) => {
1621                let key = *i;
1622                let mut inner = self.inner.borrow_mut();
1623                inner.try_raw_set_int_fast(key, v)
1624            }
1625            LuaValue::Float(f) => {
1626                let f = *f;
1627                let k_int = f as i64;
1628                if k_int as f64 == f {
1629                    let mut inner = self.inner.borrow_mut();
1630                    inner.try_raw_set_int_fast(k_int, v)
1631                } else {
1632                    self.try_raw_set_generic(k, v)
1633                }
1634            }
1635            _ => self.try_raw_set_generic(k, v),
1636        }
1637    }
1638
1639    /// Update an existing short-string slot without routing through the
1640    /// generic cold setter. Returns the value to the caller when the key is
1641    /// absent so the insertion/rehash path can account buffer growth.
1642    #[inline(always)]
1643    pub fn try_update_short_str(&self, k: &GcRef<LuaString>, v: LuaValue) -> Result<(), LuaValue> {
1644        if !k.is_short() {
1645            return Err(v);
1646        }
1647        let mut inner = self.inner.borrow_mut();
1648        inner.try_update_short_str(k, v)
1649    }
1650
1651    /// Update an existing integer slot without buffer accounting. This covers
1652    /// the stable `SETI` hot path; absent keys still fall back to the normal
1653    /// set path so array growth and hash insertion remain accounted.
1654    #[inline(always)]
1655    pub fn try_update_int(&self, k: i64, v: LuaValue) -> Result<(), LuaValue> {
1656        let mut inner = self.inner.borrow_mut();
1657        inner.try_update_int(k, v)
1658    }
1659
1660    /// Generic-key path for [`Self::try_raw_set`]. Split out so the
1661    /// integer fast path stays branch-light and inlineable.
1662    #[cold]
1663    #[inline(never)]
1664    fn try_raw_set_generic(&self, k: LuaValue, v: LuaValue) -> Result<(), LuaError> {
1665        let mut inner = self.inner.borrow_mut();
1666        if inner.array.len() + inner.node.len() >= TOTAL_GROW_CAP
1667            && matches!(inner.get_slot(&k), TableSlotRef::Absent)
1668        {
1669            return Err(LuaError::Memory);
1670        }
1671        inner.set(&k, v)
1672    }
1673
1674    /// Raw set by integer key with explicit error returns. Routes the
1675    /// array-part fast path through [`TableInner::set_int_value`] — a
1676    /// single bounds-checked store with no intermediate
1677    /// [`TableSlotRef`] indirection — and only consults the
1678    /// `TOTAL_GROW_CAP` allocation guard when the key would create a
1679    /// new slot.
1680    #[inline]
1681    pub fn try_raw_set_int(&self, k: i64, v: LuaValue) -> Result<(), LuaError> {
1682        let mut inner = self.inner.borrow_mut();
1683        inner.try_raw_set_int_fast(k, v)
1684    }
1685
1686    /// Resize the table to new array and hash sizes (sizing hint from
1687    /// the bytecode's `OP_NEWTABLE`).
1688    pub fn resize(&self, new_asize: u32, new_hsize: u32) -> Result<(), LuaError> {
1689        let mut inner = self.inner.borrow_mut();
1690        inner.resize(new_asize, new_hsize)
1691    }
1692
1693    /// Number of array-part slots currently allocated. Cheap counter
1694    /// for sizing decisions; NOT the Lua `#t` length operator.
1695    pub fn array_len(&self) -> usize {
1696        self.inner.borrow().array.len()
1697    }
1698
1699    /// Total occupied slots (array + hash) — used for legacy
1700    /// `len()` callers; prefer `getn` for the Lua `#` operator.
1701    pub fn len(&self) -> usize {
1702        let inner = self.inner.borrow();
1703        let mut n = 0usize;
1704        for v in inner.array.iter() {
1705            if !matches!(v, LuaValue::Nil) {
1706                n += 1;
1707            }
1708        }
1709        for node in inner.node.iter() {
1710            if !matches!(node.value, LuaValue::Nil) {
1711                n += 1;
1712            }
1713        }
1714        n
1715    }
1716    pub fn is_empty(&self) -> bool {
1717        self.len() == 0
1718    }
1719
1720    /// `#t` boundary (C: `luaH_getn`). Mutates internal caching state.
1721    pub fn getn(&self) -> u64 {
1722        let mut inner = self.inner.borrow_mut();
1723        inner.getn()
1724    }
1725
1726    /// Returns true iff `k` resolves to a slot in this table (array or
1727    /// hash). Used by `next` to validate the resumption key.
1728    pub fn contains_key(&self, k: &LuaValue) -> bool {
1729        if matches!(k, LuaValue::Nil) {
1730            return false;
1731        }
1732        let inner = self.inner.borrow();
1733        let slot = inner.get_slot(k);
1734        !matches!(slot, TableSlotRef::Absent)
1735    }
1736
1737    pub fn metatable(&self) -> Option<GcRef<LuaTable>> {
1738        self.metatable.get()
1739    }
1740
1741    #[inline(always)]
1742    pub fn has_metatable(&self) -> bool {
1743        self.metatable.get().is_some()
1744    }
1745
1746    /// Install a metatable. Inspects its `__mode` field eagerly so the
1747    /// GC trace impl can read [`weak_mode`] without touching the metatable
1748    /// cell again.
1749    pub fn set_metatable(&self, mt: Option<GcRef<LuaTable>>) {
1750        let mode = mt.as_ref().map(|t| extract_weak_mode(t)).unwrap_or(0);
1751        self.weak_mode.set(mode);
1752        self.metatable.set(mt);
1753    }
1754
1755    pub fn weak_mode(&self) -> u8 {
1756        self.weak_mode.get()
1757    }
1758
1759    /// Implements Lua's `next(t, k)`.
1760    pub fn next_pair(&self, k: &LuaValue) -> Option<(LuaValue, LuaValue)> {
1761        let inner = self.inner.borrow();
1762        inner.next_pair(k).ok().flatten()
1763    }
1764
1765    /// Like [`next_pair`] but reports the `"invalid key to 'next'"`
1766    /// error when `k` is non-nil and not present.
1767    pub fn try_next_pair(&self, k: &LuaValue) -> Result<Option<(LuaValue, LuaValue)>, LuaError> {
1768        let inner = self.inner.borrow();
1769        inner.next_pair(k)
1770    }
1771
1772    /// Walk every live (key, value) pair via the given closure.
1773    /// Used by the GC trace impl to avoid the overhead of repeatedly
1774    /// re-entering `find_index` from `next_pair`.
1775    /// GC traversal of a STRONG (non-weak) table, mirroring C's
1776    /// `traversehashpart` (`lgc.c`): live entries get key and value
1777    /// traced; an entry whose value is nil gets its collectable key
1778    /// TOMBSTONED (`clearkey`) so the collector may free the key object.
1779    /// The tombstone keeps the raw pointer bits for `next`-position
1780    /// matching but is never dereferenced again. Takes the inner borrow
1781    /// mutably; safe because the marker queues children instead of
1782    /// recursing, so a table's own trace never re-enters it.
1783    pub fn trace_entries_with_clearkey(&self, mut f: impl FnMut(&LuaValue)) {
1784        let mut inner = self.inner.borrow_mut();
1785        for v in inner.array.iter() {
1786            if !matches!(v, LuaValue::Nil) {
1787                f(v);
1788            }
1789        }
1790        for node in inner.node.iter_mut() {
1791            if matches!(node.value, LuaValue::Nil) {
1792                if !node.dead && gc_identity_bits(&node.key).is_some() {
1793                    node.dead = true;
1794                }
1795            } else {
1796                f(&node.key);
1797                f(&node.value);
1798            }
1799        }
1800    }
1801
1802    pub fn for_each_entry(&self, mut f: impl FnMut(&LuaValue, &LuaValue)) {
1803        let inner = self.inner.borrow();
1804        for (i, v) in inner.array.iter().enumerate() {
1805            if !matches!(v, LuaValue::Nil) {
1806                let k = LuaValue::Int((i + 1) as i64);
1807                f(&k, v);
1808            }
1809        }
1810        for node in inner.node.iter() {
1811            if !matches!(node.value, LuaValue::Nil) {
1812                f(&node.key, &node.value);
1813            }
1814        }
1815    }
1816
1817    /// Drop weak entries whose weakly-tracked target is unreachable,
1818    /// and return the list of values whose strings must still be
1819    /// marked by the caller.
1820    pub fn prune_weak_dead(&self, is_reachable: &dyn Fn(usize) -> bool) -> Vec<LuaValue> {
1821        self.prune_weak_dead_with(is_reachable, is_reachable)
1822    }
1823
1824    /// Variant of [`Self::prune_weak_dead`] that allows key and value sides to
1825    /// use different liveness predicates. Lua keeps objects pending finalization
1826    /// visible as weak keys until their `__gc` runs, but clears them from weak
1827    /// values before the finalizer.
1828    pub fn prune_weak_dead_with(
1829        &self,
1830        is_key_reachable: &dyn Fn(usize) -> bool,
1831        is_value_reachable: &dyn Fn(usize) -> bool,
1832    ) -> Vec<LuaValue> {
1833        self.prune_weak_dead_with_value(
1834            &|v| collectable_identity(v).map_or(true, is_key_reachable),
1835            &|v| collectable_identity(v).map_or(true, is_value_reachable),
1836        )
1837    }
1838
1839    /// Value-aware weak cleanup. Generational minor collection uses this to
1840    /// treat unmarked old values as live, because young sweep will not free
1841    /// them even when the minor marker skipped their subgraph.
1842    ///
1843    /// Hash nodes whose value is already nil (manually erased entries) get
1844    /// their collectable key TOMBSTONED, mirroring C's unconditional
1845    /// `clearkey` of empty entries in `clearbykeys`/`clearbyvalues`
1846    /// (`lgc.c`). Skipping them leaves a never-again-traced key ref in the
1847    /// node; once the key object is swept, a later probe content-compares
1848    /// freed memory (found by the rooting battery on gc.lua, 2026-06-10).
1849    pub fn prune_weak_dead_with_value(
1850        &self,
1851        is_key_reachable: &dyn Fn(&LuaValue) -> bool,
1852        is_value_reachable: &dyn Fn(&LuaValue) -> bool,
1853    ) -> Vec<LuaValue> {
1854        let mode = self.weak_mode.get();
1855        if mode == 0 {
1856            return Vec::new();
1857        }
1858        let weak_k = (mode & WEAK_KEYS) != 0;
1859        let weak_v = (mode & WEAK_VALUES) != 0;
1860        let mut to_mark: Vec<LuaValue> = Vec::new();
1861        let mut inner = self.inner.borrow_mut();
1862        for i in 0..inner.array.len() {
1863            let v = inner.array[i].clone();
1864            if matches!(v, LuaValue::Nil) {
1865                continue;
1866            }
1867            if weak_v && value_is_dead_collectable(&v, is_value_reachable) {
1868                inner.array[i] = LuaValue::Nil;
1869                continue;
1870            }
1871            if weak_v {
1872                if matches!(v, LuaValue::Str(_)) {
1873                    to_mark.push(v);
1874                }
1875            }
1876        }
1877        let mut i = 0;
1878        while i < inner.node.len() {
1879            let v = inner.node[i].value.clone();
1880            if matches!(v, LuaValue::Nil) {
1881                if !inner.node[i].dead && gc_identity_bits(&inner.node[i].key).is_some() {
1882                    inner.node[i].dead = true;
1883                }
1884                i += 1;
1885                continue;
1886            }
1887            let k = inner.node[i].key.clone();
1888            if weak_v && value_is_dead_collectable(&v, is_value_reachable) {
1889                inner.clear_dead_hash_node(i);
1890                continue;
1891            }
1892            if weak_k && value_is_dead_collectable(&k, is_key_reachable) {
1893                inner.clear_dead_hash_node(i);
1894                continue;
1895            }
1896            if weak_k {
1897                if matches!(k, LuaValue::Str(_)) {
1898                    to_mark.push(k);
1899                }
1900            }
1901            if weak_v {
1902                if matches!(v, LuaValue::Str(_)) {
1903                    to_mark.push(v);
1904                }
1905            }
1906            i += 1;
1907        }
1908        to_mark
1909    }
1910
1911    /// Ephemeron-convergence helper for pure `__mode = "k"` tables.
1912    pub fn ephemeron_values_to_mark(&self, is_reachable: &dyn Fn(usize) -> bool) -> Vec<LuaValue> {
1913        self.ephemeron_values_to_mark_with_value(&|v| {
1914            collectable_identity(v).map_or(true, is_reachable)
1915        })
1916    }
1917
1918    /// Value-aware ephemeron helper for minor collections, where unmarked old
1919    /// keys are still live.
1920    pub fn ephemeron_values_to_mark_with_value(
1921        &self,
1922        is_reachable: &dyn Fn(&LuaValue) -> bool,
1923    ) -> Vec<LuaValue> {
1924        let mode = self.weak_mode.get();
1925        if (mode & WEAK_KEYS) == 0 || (mode & WEAK_VALUES) != 0 {
1926            return Vec::new();
1927        }
1928        let inner = self.inner.borrow();
1929        let mut out = Vec::new();
1930        for node in inner.node.iter() {
1931            if matches!(node.value, LuaValue::Nil) {
1932                continue;
1933            }
1934            if !value_is_dead_collectable(&node.key, is_reachable) {
1935                out.push(node.value.clone());
1936            }
1937        }
1938        for (i, v) in inner.array.iter().enumerate() {
1939            if matches!(v, LuaValue::Nil) {
1940                continue;
1941            }
1942            let k = LuaValue::Int((i + 1) as i64);
1943            if !value_is_dead_collectable(&k, is_reachable) {
1944                out.push(v.clone());
1945            }
1946        }
1947        out
1948    }
1949}
1950
1951// ── Free helpers ──────────────────────────────────────────────────────────────
1952
1953/// True iff `v` is a collectable non-string LuaValue whose target was
1954/// unreached during the mark phase. Strings are explicitly excluded.
1955fn value_is_dead_collectable(v: &LuaValue, is_reachable: &dyn Fn(&LuaValue) -> bool) -> bool {
1956    collectable_identity(v).is_some() && !is_reachable(v)
1957}
1958
1959/// Raw pointer bits of any collectable value, WITHOUT dereferencing the
1960/// target — safe to call on a dead-key tombstone whose object was freed.
1961fn gc_identity_bits(v: &LuaValue) -> Option<usize> {
1962    match v {
1963        LuaValue::Str(x) => Some(x.identity()),
1964        LuaValue::Table(x) => Some(x.identity()),
1965        LuaValue::UserData(x) => Some(x.identity()),
1966        LuaValue::Thread(x) => Some(x.identity()),
1967        LuaValue::Function(LuaClosure::Lua(x)) => Some(x.identity()),
1968        LuaValue::Function(LuaClosure::C(x)) => Some(x.identity()),
1969        LuaValue::Function(LuaClosure::LightC(_))
1970        | LuaValue::Nil
1971        | LuaValue::Bool(_)
1972        | LuaValue::Int(_)
1973        | LuaValue::Float(_)
1974        | LuaValue::LightUserData(_) => None,
1975    }
1976}
1977
1978fn collectable_identity(v: &LuaValue) -> Option<usize> {
1979    match v {
1980        LuaValue::Table(t) => Some(t.identity()),
1981        LuaValue::UserData(u) => Some(u.identity()),
1982        LuaValue::Thread(th) => Some(th.identity()),
1983        LuaValue::Function(c) => match c {
1984            LuaClosure::Lua(x) => Some(x.identity()),
1985            LuaClosure::C(x) => Some(x.identity()),
1986            LuaClosure::LightC(_) => None,
1987        },
1988        LuaValue::Str(_)
1989        | LuaValue::Nil
1990        | LuaValue::Bool(_)
1991        | LuaValue::Int(_)
1992        | LuaValue::Float(_)
1993        | LuaValue::LightUserData(_) => None,
1994    }
1995}
1996
1997/// Inspect a metatable's `__mode` field and produce the corresponding
1998/// `WEAK_KEYS | WEAK_VALUES` bitmask. Returns 0 when no `__mode` is
1999/// set or it is not a string.
2000fn extract_weak_mode(mt: &LuaTable) -> u8 {
2001    let inner = mt.inner.borrow();
2002    for node in inner.node.iter() {
2003        if node.dead {
2004            continue;
2005        }
2006        if let LuaValue::Str(ks) = &node.key {
2007            if ks.as_bytes() == b"__mode" {
2008                if let LuaValue::Str(vs) = &node.value {
2009                    let bytes = vs.as_bytes();
2010                    let mut mode = 0u8;
2011                    if bytes.iter().any(|b| *b == b'k') {
2012                        mode |= WEAK_KEYS;
2013                    }
2014                    if bytes.iter().any(|b| *b == b'v') {
2015                        mode |= WEAK_VALUES;
2016                    }
2017                    return mode;
2018                }
2019                return 0;
2020            }
2021        }
2022    }
2023    0
2024}
2025
2026// ──────────────────────────────────────────────────────────────────────────────
2027// PORT STATUS
2028//   source:        src/ltable.c (~995 lines, 28 functions), src/ltable.h
2029//   target_crate:  lua-types
2030//   confidence:    high
2031//   todos:         0
2032//   port_notes:    0
2033//   unsafe_blocks: 0
2034//   notes:         Canonical LuaTable: hybrid array + hash. Mirrors C's Table
2035//                  struct (flags, lsizenode, alimit, array, node, lastfree) with
2036//                  Vec<LuaValue> + Vec<TableNode> in place of raw C pointers, and
2037//                  Option<usize> indexing in place of Node*. The luaH_getn
2038//                  boundary search + alimit-aware integer-key fast path are
2039//                  ported faithfully (see getn() and get_int_slot()). The
2040//                  integer-key read path also exposes get_int_value, which
2041//                  mirrors C's luaH_getint by returning the array slot directly
2042//                  in one bounds-checked load (no TableSlotRef indirection) and
2043//                  splitting the rare alimit-aliased / hash-part path into a
2044//                  cold helper. The short-string read path mirrors that shape
2045//                  via get_str_value (single hash-chain walk, no TableSlotRef
2046//                  round-trip); LuaTable::get dispatches on integer/short-string
2047//                  keys inline and routes everything else through a #[cold]
2048//                  get_generic_value, matching C's luaH_get fast-path structure.
2049//                  LuaTable::get / get_int / get_short_str are #[inline(always)]
2050//                  so the dispatch folds into the cross-crate VM hot frames
2051//                  (state::fast_get / state::table_get_with_tm). Weak-table mode
2052//                  flags + the prune_weak_dead / ephemeron_values_to_mark
2053//                  helpers integrate with the lua-gc Trace impl.
2054// ──────────────────────────────────────────────────────────────────────────────