Skip to main content

lua_types/
table.rs

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