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