Skip to main content

luna_core/runtime/
table.rs

1//! Lua table: hybrid array + hash.
2//!
3//! Array part uses split tag/payload storage (9 bytes/slot — the Lua 5.5
4//! "compact arrays" layout, bench-validated in benches/value_repr.rs).
5//! Hash part is the PUC node layout: main-position chaining with relocation
6//! (Brent's variation), capacity a power of two, rehash sizing per
7//! luaH_rehash/computesizes.
8
9use crate::runtime::heap::{Gc, GcHeader, Heap, Marker};
10use crate::runtime::value::{RawVal, Value, f2i_exact, raw};
11
12/// Errors that table mutation can raise back to the interpreter.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum TableError {
15    /// `t[nil] = …` — `nil` is forbidden as a key.
16    NilIndex,
17    /// `t[0/0] = …` — NaN floats are forbidden as keys.
18    NanIndex,
19    /// `next` called with a key not present in the table.
20    InvalidNext,
21    /// PUC `luaH_resizearray` — the array part would have to grow past
22    /// `MAXASIZE`, or the hash part past `MAXHBITS`. Raised back as
23    /// "table overflow" so a runaway `a[i] = i` loop walls within budget
24    /// (5.5/5.4 heavy.lua's `toomanyidx` pcalls exactly this scenario).
25    Overflow,
26}
27
28/// PUC `MAXASIZE` analogue: the highest power of two an array part may
29/// grow to. Choose a cap that comfortably fits in the gate's 60-second
30/// budget (each grow is O(n), so 2^27 entries × 16 bytes ≈ 2 GB is the
31/// effective ceiling). Beyond this `rehash` returns `TableError::Overflow`.
32pub(crate) const MAX_ASIZE: usize = 1 << 27;
33
34#[derive(Clone, Copy)]
35pub(crate) struct Node {
36    key: Value,
37    val: Value,
38    /// absolute index of the next node in this chain, or NONE
39    next: i32,
40    /// PUC `setdeadkey` analogue: the key was a collectable that got swept
41    /// out of a weak table. The Gc pointer in `key` is now dangling — its
42    /// memory may have been reused for a new allocation with potentially
43    /// equal content. Marking the node "dead-key" lets `find_node` skip the
44    /// raw_eq probe (which could spuriously match a reallocated object) and
45    /// `insert_new` treat the slot as available for a fresh main-position
46    /// owner while leaving chain back-links intact for traversal.
47    dead_key: bool,
48}
49
50const NONE: i32 = -1;
51
52impl Node {
53    const EMPTY: Node = Node {
54        key: Value::Nil,
55        val: Value::Nil,
56        next: NONE,
57        dead_key: false,
58    };
59}
60
61/// P11-S5d.I — inline storage threshold. Tables whose array part has
62/// `asize <= INLINE_ASIZE` keep their atags+avals inside the Table
63/// struct itself (`inline_storage`), skipping the slab Box entirely
64/// — binary_trees's `{nil, nil}` and `{...}` 2-element leaves live
65/// here, sparing one allocator round-trip per NewTable.
66pub(crate) const INLINE_ASIZE: u64 = 2;
67/// `INLINE_ASIZE` u64 slots for avals + `ceil(INLINE_ASIZE / 8)` u64
68/// slots covering the atags bytes (with trailing pad). For
69/// `INLINE_ASIZE = 2`: 2 avals + 1 atags = 3 u64s = 24 bytes.
70pub(crate) const INLINE_U64S: usize = INLINE_ASIZE as usize + INLINE_ASIZE.div_ceil(8) as usize;
71
72/// Lua table — hybrid array + hash storage, with optional metatable and
73/// weak-mode flags.
74#[repr(C)]
75pub struct Table {
76    /// read through raw casts by the GC, not by field access
77    #[allow(dead_code)]
78    pub(crate) hdr: GcHeader,
79    /// P11-S5d.I — single backing pointer for the array part. Points
80    /// to `inline_storage` (asize <= INLINE_ASIZE) or `slab.as_ptr()`
81    /// (asize > INLINE_ASIZE). The JIT inline aset reads this with one
82    /// `load i64`, no branch — the choice between inline and slab is
83    /// already encoded in the pointer. Initialised in `Heap::new_table`
84    /// AFTER the Table reaches its final heap address (so that
85    /// `&mut self.inline_storage` is the stable heap pointer, not a
86    /// stack-local one). Updated by `Table::resize`.
87    pub array_ptr: *mut u8,
88    /// P11-S5d.H — external backing for the array part when
89    /// `asize > INLINE_ASIZE`. Layout: `[avals: asize × 8 bytes][atags:
90    /// asize bytes]`. Empty box (dangling, no alloc) when the inline
91    /// path is in use.
92    pub(crate) slab: Box<[u64]>,
93    /// Length of the array part in slots. u64 (rather than `usize` or
94    /// `u32`) so the JIT can load it with a single `load i64`.
95    pub asize: u64,
96    /// P11-S5d.I — inline backing used when `asize <= INLINE_ASIZE`.
97    /// Same layout as the slab: avals at low addresses (`asize * 8`
98    /// bytes from offset 0), atags at the trailing `asize` bytes.
99    pub(crate) inline_storage: [u64; INLINE_U64S],
100    /// hash part: power-of-two length (or empty)
101    /// hash part: power-of-two length (or empty)
102    /// `pub(crate)` so `Heap::free_obj` (pool recycle path) can reset.
103    pub(crate) nodes: Box<[Node]>,
104    /// free-slot search position, counts down (PUC lastfree).
105    /// `pub(crate)` so `Heap::new_table` can reset on pool recycle.
106    pub(crate) lastfree: u32,
107    /// P11-S5d.K — visibility lifted to `pub(crate)` so the JIT can
108    /// take its field offset at compile time and emit an inline
109    /// "metatable.is_none()" guard before the inline aget fast path.
110    /// `Option<Gc<Table>>` is 8 bytes via the NonNull-pointer-opt: 0
111    /// ⇔ None, non-zero ⇔ Some.
112    pub metatable: Option<Gc<Table>>,
113    /// reserved for an absent-metamethod cache (PUC `flags`); currently
114    /// unread — luna's mm lookup walks `metatable.get` each time
115    #[allow(dead_code)]
116    pub(crate) flags: u8,
117}
118
119// SAFETY: `array_ptr` looks like an unprotected raw pointer field, but
120// it always refers to memory the same Table owns (either its own inline
121// storage or its `slab` Box). The Table is heap-allocated and never
122// moved post-adoption, so the pointer stays valid for the table's
123// lifetime. No thread-unsafety concern: tables are accessed only
124// through the Vm, single-threaded.
125unsafe impl Send for Table {}
126unsafe impl Sync for Table {}
127
128impl Table {
129    pub(crate) fn new(hdr: GcHeader) -> Table {
130        Table {
131            hdr,
132            // P11-S5d.I — `array_ptr` is fixed up in
133            // `Heap::new_table` after the Table reaches its final heap
134            // address (so that `&inline_storage` is the heap address,
135            // not a stack-local one). Null sentinel here so a
136            // bug-detection invariant flags any pre-fixup read.
137            array_ptr: std::ptr::null_mut(),
138            slab: Box::new([]),
139            asize: 0,
140            inline_storage: [0; INLINE_U64S],
141            nodes: Box::new([]),
142            lastfree: 0,
143            metatable: None,
144            flags: 0,
145        }
146    }
147
148    /// P11-S5d.I — set `array_ptr` to the inline storage's stable heap
149    /// address. Called by `Heap::new_table` once the Table is at its
150    /// final location.
151    #[inline]
152    pub(crate) fn init_array_ptr(&mut self) {
153        self.array_ptr = self.inline_storage.as_mut_ptr() as *mut u8;
154    }
155
156    /// P11-S5d.H/I — read view onto the array-part tag bytes. Trails
157    /// the avals portion in the active backing (inline or slab).
158    #[inline(always)]
159    pub(crate) fn atags(&self) -> &[u8] {
160        let n = self.asize as usize;
161        if n == 0 {
162            return &[];
163        }
164        // SAFETY: `array_ptr` always points to a buffer with `n`
165        // RawVal slots followed by `n` u8 tag bytes (either
166        // `inline_storage` of `INLINE_U64S` u64s, or a `slab` of
167        // `asize + ceil(asize/8)` u64s). The tag bytes start at byte
168        // offset `n * 8` from the buffer base.
169        unsafe {
170            let ptr = self.array_ptr.add(n * 8);
171            std::slice::from_raw_parts(ptr, n)
172        }
173    }
174
175    #[inline(always)]
176    pub(crate) fn atags_mut(&mut self) -> &mut [u8] {
177        let n = self.asize as usize;
178        if n == 0 {
179            return &mut [];
180        }
181        // SAFETY: `array_ptr` was allocated by `Heap::init_array_ptr` with `array_cap` slots; the table holds it for its lifetime and the heap is single-threaded so no concurrent writers exist.
182        unsafe {
183            let ptr = self.array_ptr.add(n * 8);
184            std::slice::from_raw_parts_mut(ptr, n)
185        }
186    }
187
188    /// P11-S5d.H/I — read view onto the array-part payload slots. Sits
189    /// at the start of the active backing (u64-aligned, identical size
190    /// and layout to `RawVal`).
191    #[inline(always)]
192    pub(crate) fn avals(&self) -> &[RawVal] {
193        let n = self.asize as usize;
194        if n == 0 {
195            return &[];
196        }
197        // SAFETY: inline_storage / slab both store u64s, so the cast
198        // to `*const RawVal` is alignment-safe (RawVal size = 8,
199        // align = 8). The buffer holds at least `n` such slots.
200        unsafe { std::slice::from_raw_parts(self.array_ptr as *const RawVal, n) }
201    }
202
203    #[inline(always)]
204    pub(crate) fn avals_mut(&mut self) -> &mut [RawVal] {
205        let n = self.asize as usize;
206        if n == 0 {
207            return &mut [];
208        }
209        // SAFETY: `array_ptr` was allocated by `Heap::init_array_ptr` with `array_cap` slots; the table holds it for its lifetime and the heap is single-threaded so no concurrent writers exist.
210        unsafe { std::slice::from_raw_parts_mut(self.array_ptr as *mut RawVal, n) }
211    }
212
213    /// Allocate a fresh external `[avals: asize × 8 bytes][atags: asize
214    /// bytes]` slab. Only used when `asize > INLINE_ASIZE`. The buffer
215    /// is u64-aligned via `Box<[u64]>` and zeroed (avals = `RawVal::
216    /// NIL` aka `0`; atags = `raw::NIL` aka `0`).
217    fn alloc_slab(asize: usize) -> Box<[u64]> {
218        if asize == 0 {
219            return Box::new([]);
220        }
221        let avals_u64s = asize;
222        let atags_u64s = asize.div_ceil(8);
223        let total = avals_u64s + atags_u64s;
224        vec![0u64; total].into_boxed_slice()
225    }
226
227    /// This table's metatable, if any.
228    pub fn metatable(&self) -> Option<Gc<Table>> {
229        self.metatable
230    }
231
232    /// Install (or clear) this table's metatable. Does not perform any
233    /// `__metatable` guarding; that belongs in the Vm-level `setmetatable`.
234    pub fn set_metatable(&mut self, mt: Option<Gc<Table>>) {
235        self.metatable = mt;
236    }
237
238    /// Bytes occupied by the table's *external* internal allocations
239    /// (slab and nodes). Cheap O(1) read — Box len × element size, no
240    /// allocator query. `Heap::free_obj` subtracts this on the way out
241    /// so the credit applied via `set`/`rehash`/`ensure_*` is symmetric.
242    ///
243    /// P11-S5d.I — inline storage doesn't count toward this (it's part
244    /// of the Table struct itself, accounted for by `size_of::<Table>()`
245    /// at adoption time). When the array part lives inline, the slab
246    /// is empty and contributes nothing here.
247    pub(crate) fn internal_bytes(&self) -> usize {
248        let n = self.asize as usize;
249        let array_external = if n > INLINE_ASIZE as usize {
250            n + n * std::mem::size_of::<RawVal>()
251        } else {
252            0
253        };
254        array_external + self.nodes.len() * std::mem::size_of::<Node>()
255    }
256
257    fn asize(&self) -> usize {
258        self.asize as usize
259    }
260
261    fn aget(&self, idx: usize) -> Value {
262        // SAFETY: callers gate on `idx < self.asize()` before reaching here
263        // (`get_int`, `iter_array`, etc.). atags and avals are sized
264        // identically by `rehash`, so a bound check passed against atags
265        // covers avals too.
266        unsafe {
267            Value::pack(
268                *self.atags().get_unchecked(idx),
269                *self.avals().get_unchecked(idx),
270            )
271        }
272    }
273
274    fn aset(&mut self, idx: usize, v: Value) {
275        let (t, b) = v.unpack();
276        // SAFETY: see `aget`. callers (`set_norm`, `set_int`) gate on
277        // `idx < self.asize()`. The two `*_mut` calls each take a
278        // distinct `&mut self` borrow whose lifetime ends at the
279        // statement boundary, so they don't overlap.
280        unsafe {
281            *self.atags_mut().get_unchecked_mut(idx) = t;
282            *self.avals_mut().get_unchecked_mut(idx) = b;
283        }
284    }
285
286    // ---- reads ----
287
288    /// Raw lookup (no `__index` metamethod). Returns `Value::Nil` when
289    /// the key is absent. `Value::Nil` and NaN floats return `nil` directly.
290    pub fn get(&self, key: Value) -> Value {
291        match key {
292            Value::Int(i) => self.get_int(i),
293            Value::Float(f) => match f2i_exact(f) {
294                Some(i) => self.get_int(i),
295                None => {
296                    if f.is_nan() {
297                        Value::Nil
298                    } else {
299                        self.get_hash(key)
300                    }
301                }
302            },
303            Value::Nil => Value::Nil,
304            k => self.get_hash(k),
305        }
306    }
307
308    /// Integer-keyed variant of [`Self::get`].
309    pub fn get_int(&self, i: i64) -> Value {
310        if i >= 1 && (i as u64) <= self.asize() as u64 {
311            return self.aget(i as usize - 1);
312        }
313        self.get_hash(Value::Int(i))
314    }
315
316    /// String-keyed variant of [`Self::get`] for v1.2 D4 A1 GetField fast
317    /// path: the GetField interp arm always has a `Gc<LuaStr>` key from
318    /// `Proto.consts`. Skips the outer `Value` match (which would only
319    /// take the `_ => self.get_hash(k)` arm anyway) so the dispatcher
320    /// pays one less branch per call. ~5 GetField/iter × 1000 iters/cell
321    /// on the Redis-Lua-shape workload — every shaved nanosecond shows
322    /// up at the bench level. Counter-validated via
323    /// `examples/diag_opcode_breakdown.rs`.
324    #[inline]
325    pub fn get_str(&self, key: crate::runtime::Gc<crate::runtime::string::LuaStr>) -> Value {
326        self.get_hash(Value::Str(key))
327    }
328
329    fn get_hash(&self, k: Value) -> Value {
330        match self.find_node(k) {
331            Some(idx) => self.nodes[idx].val,
332            None => Value::Nil,
333        }
334    }
335
336    /// Walk the chain rooted at the key's main position.
337    fn find_node(&self, k: Value) -> Option<usize> {
338        if self.nodes.is_empty() {
339            return None;
340        }
341        let mut idx = self.main_position(k);
342        loop {
343            let n = &self.nodes[idx];
344            // Dead-key slots carry a dangling Gc pointer whose memory may
345            // have been reallocated to a different live object; raw_eq on
346            // such a key can spuriously match the freshly-reused address.
347            // Skip the comparison and only follow `next` (PUC `setdeadkey`
348            // / `equalkey` short-circuit). 5.5 gc.lua :459-:478 was 12%
349            // flaky on this exact path — a swept B-string's slot kept
350            // chaining into A's slot, so `a[k] = nil` (k = A_string) hit
351            // the dead slot and wrote nil there, leaving A's val untouched.
352            if !n.dead_key && n.key.raw_eq(k) {
353                return Some(idx);
354            }
355            if n.next == NONE {
356                return None;
357            }
358            idx = n.next as usize;
359        }
360    }
361
362    // ---- writes ----
363
364    /// Insert / update `(key, val)`. `heap` is used to credit any internal
365    /// Box growth (rehash) to `heap.bytes` so the counter stays in sync with
366    /// real memory; `free_obj` subtracts `internal_bytes()` on the way out.
367    pub fn set(&mut self, heap: &mut Heap, key: Value, val: Value) -> Result<(), TableError> {
368        let k = normalize_set_key(key)?;
369        self.set_norm(heap, k, val)
370    }
371
372    /// Integer-keyed variant of [`Self::set`].
373    pub fn set_int(&mut self, heap: &mut Heap, i: i64, val: Value) -> Result<(), TableError> {
374        self.set_norm(heap, Value::Int(i), val)
375    }
376
377    /// `k` is already normalized (no nil, no NaN, integral floats → Int).
378    fn set_norm(&mut self, heap: &mut Heap, k: Value, v: Value) -> Result<(), TableError> {
379        if let Value::Int(i) = k
380            && i >= 1
381            && (i as u64) <= self.asize() as u64
382        {
383            self.aset(i as usize - 1, v);
384            return Ok(());
385        }
386        if let Some(idx) = self.find_node(k) {
387            self.nodes[idx].val = v;
388            return Ok(());
389        }
390        if v.is_nil() {
391            return Ok(()); // absent key set to nil: nothing to record
392        }
393        self.insert_new(heap, k, v)
394    }
395
396    fn insert_new(&mut self, heap: &mut Heap, k: Value, v: Value) -> Result<(), TableError> {
397        if self.nodes.is_empty() {
398            self.rehash(heap, k)?;
399            return self.set_norm(heap, k, v);
400        }
401        let mp = self.main_position(k);
402        // A truly empty slot (key=Nil, !dead_key) is free for direct placement.
403        // A dead-key slot still belongs to some chain (its `next` points to a
404        // live entry the chain reaches), so we treat it as occupied here and
405        // route the new key through the collision path below — that preserves
406        // the back-links into this slot from other nodes' `next` fields.
407        if self.nodes[mp].key.is_nil() && !self.nodes[mp].dead_key {
408            self.nodes[mp] = Node {
409                key: k,
410                val: v,
411                next: NONE,
412                dead_key: false,
413            };
414            return Ok(());
415        }
416        let Some(free) = self.free_pos() else {
417            self.rehash(heap, k)?;
418            return self.set_norm(heap, k, v);
419        };
420        // Dead-key slot: it carries no live key, so by definition nobody else
421        // counts it as "their main position owner". We give it directly to
422        // the new key but preserve `next` so the chain it sits inside still
423        // reaches its downstream entries.
424        if self.nodes[mp].dead_key {
425            let preserved_next = self.nodes[mp].next;
426            self.nodes[mp] = Node {
427                key: k,
428                val: v,
429                next: preserved_next,
430                dead_key: false,
431            };
432            return Ok(());
433        }
434        let other_mp = self.main_position(self.nodes[mp].key);
435        if other_mp != mp {
436            // colliding node is out of its main position: relocate it to the
437            // free slot and take its place
438            let mut prev = other_mp;
439            while self.nodes[prev].next != mp as i32 {
440                prev = self.nodes[prev].next as usize;
441            }
442            self.nodes[prev].next = free as i32;
443            self.nodes[free] = self.nodes[mp];
444            self.nodes[mp] = Node {
445                key: k,
446                val: v,
447                next: NONE,
448                dead_key: false,
449            };
450        } else {
451            // colliding node owns this position: chain the new node behind it
452            self.nodes[free] = Node {
453                key: k,
454                val: v,
455                next: self.nodes[mp].next,
456                dead_key: false,
457            };
458            self.nodes[mp].next = free as i32;
459        }
460        Ok(())
461    }
462
463    fn free_pos(&mut self) -> Option<usize> {
464        while self.lastfree > 0 {
465            self.lastfree -= 1;
466            let n = &self.nodes[self.lastfree as usize];
467            // Dead-key slots are still occupied for chain purposes (their
468            // `next` may be the only path to a downstream entry) — don't
469            // hand them out as free.
470            if n.key.is_nil() && !n.dead_key {
471                return Some(self.lastfree as usize);
472            }
473        }
474        None
475    }
476
477    // ---- rehash (PUC luaH_rehash) ----
478
479    fn rehash(&mut self, heap: &mut Heap, pending: Value) -> Result<(), TableError> {
480        let mut nums = [0usize; 65];
481        let mut int_keys = 0usize;
482        let mut total = 1; // the pending key
483        if let Value::Int(i) = pending
484            && i >= 1
485        {
486            nums[ceil_log2(i as u64)] += 1;
487            int_keys += 1;
488        }
489        let atags = self.atags();
490        for (i, &tag) in atags.iter().enumerate() {
491            if tag != raw::NIL {
492                nums[ceil_log2(i as u64 + 1)] += 1;
493                int_keys += 1;
494                total += 1;
495            }
496        }
497        for n in self.nodes.iter() {
498            if !n.val.is_nil() {
499                total += 1;
500                if let Value::Int(i) = n.key
501                    && i >= 1
502                {
503                    nums[ceil_log2(i as u64)] += 1;
504                    int_keys += 1;
505                }
506            }
507        }
508        // computesizes: optimal array size = largest 2^i with more than 2^(i-1)
509        // integer keys in [1, 2^i]
510        let mut new_asize = 0usize;
511        let mut in_array = 0usize;
512        let mut a = 0usize;
513        let mut two_to_i = 1usize;
514        let mut i = 0usize;
515        while int_keys > two_to_i / 2 {
516            a += nums[i];
517            if a > two_to_i / 2 {
518                new_asize = two_to_i;
519                in_array = a;
520            }
521            i += 1;
522            match two_to_i.checked_mul(2) {
523                Some(n) => two_to_i = n,
524                None => break,
525            }
526        }
527        // PUC `luaH_resizearray` raises "table overflow" when the array part
528        // would have to grow past MAXASIZE. luna mirrors with `MAX_ASIZE`,
529        // checked on both the array and the hash bucket count (the latter is
530        // a power-of-two of total - in_array entries).
531        if new_asize > MAX_ASIZE {
532            return Err(TableError::Overflow);
533        }
534        let hash_entries = total - in_array;
535        if hash_entries > MAX_ASIZE {
536            return Err(TableError::Overflow);
537        }
538        self.resize(heap, new_asize, hash_entries);
539        Ok(())
540    }
541
542    /// Resize the table's array and hash parts. The array part grows
543    /// (or shrinks) to `new_asize` NIL-initialized slots; the hash
544    /// part rounds to the next power of two ≥ `hash_entries`. Any
545    /// existing entries are re-inserted into the new layout. The
546    /// Box growth is debited/credited to `heap.bytes` so `free_obj`
547    /// can subtract the symmetric amount.
548    ///
549    /// P11-S5c.B — `Heap::new_table_sized` calls this on a freshly
550    /// adopted empty table to pre-allocate the array part, sparing
551    /// the table-fill loop from O(log N) intermediate `rehash`es.
552    pub(crate) fn resize(&mut self, heap: &mut Heap, new_asize: usize, hash_entries: usize) {
553        let before = self.internal_bytes();
554        // P11-S5d.H/I — snapshot the old array entries before we
555        // re-install the backing. The active buffer can be inline OR
556        // slab; `array_ptr` already points to whichever it is, so
557        // walking via raw offsets works the same for either case.
558        let old_asize = self.asize as usize;
559        let mut old_pairs: Vec<(u8, RawVal)> = Vec::with_capacity(old_asize);
560        if old_asize > 0 {
561            // SAFETY: `array_ptr` was set up by `Heap::new_table` or
562            // an earlier `resize`; it covers `old_asize * 9` bytes
563            // (avals + atags).
564            let avals_base = self.array_ptr as *const RawVal;
565            let atags_base = unsafe { self.array_ptr.add(old_asize * 8) as *const u8 };
566            for i in 0..old_asize {
567                // SAFETY: `i < array_len` is enforced by the surrounding loop bound; `atags_base` / `avals_base` point into the table's parallel arrays allocated in lockstep by `init_array_ptr`.
568                let tag = unsafe { *atags_base.add(i) };
569                // SAFETY: `i < array_len` is enforced by the surrounding loop bound; `atags_base` / `avals_base` point into the table's parallel arrays allocated in lockstep by `init_array_ptr`.
570                let val = unsafe { *avals_base.add(i) };
571                old_pairs.push((tag, val));
572            }
573        }
574        let old_nodes = std::mem::take(&mut self.nodes);
575
576        // Install the new array backing first, then update `array_ptr`
577        // (before potentially dropping the old slab via the assignment
578        // below) so the JIT never observes a stale pointer.
579        self.asize = new_asize as u64;
580        if new_asize <= INLINE_ASIZE as usize {
581            // Inline path — zero the inline buffer; drop any prior
582            // external slab.
583            for slot in self.inline_storage.iter_mut() {
584                *slot = 0;
585            }
586            self.array_ptr = self.inline_storage.as_mut_ptr() as *mut u8;
587            self.slab = Box::new([]);
588        } else {
589            // External slab — allocate, then re-point `array_ptr`.
590            self.slab = Self::alloc_slab(new_asize);
591            self.array_ptr = self.slab.as_mut_ptr() as *mut u8;
592        }
593
594        let hsize = if hash_entries == 0 {
595            0
596        } else {
597            hash_entries.next_power_of_two()
598        };
599        self.nodes = vec![Node::EMPTY; hsize].into_boxed_slice();
600        self.lastfree = hsize as u32;
601        // PUC `g->GCtotalbytes` analogue: credit (or debit) the box-size
602        // delta so `Heap.bytes` reflects this table's actual internal
603        // memory. `free_obj` subtracts `internal_bytes()` on the way out.
604        let after = self.internal_bytes();
605        heap.apply_bytes_delta(before, after);
606        // Re-insert old array entries via the public set_norm path
607        // (which handles rehashing if the new array shrinks below the
608        // entry count).
609        for (i, (tag, val)) in old_pairs.into_iter().enumerate() {
610            if tag != raw::NIL {
611                // SAFETY: `tag` and the raw value come from this table's parallel `atags` / `avals` arrays, which the table writers always keep in sync — the tag byte matches the raw payload's discriminator (see `runtime::value` `raw` module).
612                let v = unsafe { Value::pack(tag, val) };
613                let _ = self.set_norm(heap, Value::Int(i as i64 + 1), v);
614            }
615        }
616        for n in old_nodes.iter() {
617            if !n.val.is_nil() {
618                let _ = self.set_norm(heap, n.key, n.val);
619            }
620        }
621    }
622
623    fn main_position(&self, k: Value) -> usize {
624        debug_assert!(!self.nodes.is_empty());
625        hash_key(k) as usize & (self.nodes.len() - 1)
626    }
627
628    // ---- length / iteration ----
629
630    /// A border: `n` where `t[n]` is non-nil and `t[n+1]` is nil (PUC `luaH_getn`).
631    /// This is Lua `#` semantics, not a container size — an `is_empty`
632    /// counterpart would be meaningless.
633    #[allow(clippy::len_without_is_empty)]
634    pub fn len(&self) -> i64 {
635        let asize = self.asize();
636        let atags = self.atags();
637        if asize > 0 && atags[asize - 1] == raw::NIL {
638            // binary search inside the array part
639            let (mut lo, mut hi) = (0usize, asize);
640            while hi - lo > 1 {
641                let m = lo + (hi - lo) / 2;
642                if atags[m - 1] == raw::NIL {
643                    hi = m;
644                } else {
645                    lo = m;
646                }
647            }
648            return lo as i64;
649        }
650        if self.nodes.is_empty() {
651            return asize as i64;
652        }
653        // array is full (or absent): unbound search through the hash part
654        let mut lo = asize as i64;
655        let mut hi = lo + 1;
656        while !self.get_int(hi).is_nil() {
657            lo = hi;
658            match hi.checked_mul(2) {
659                Some(n) => hi = n,
660                None => {
661                    // pathological sparse keys (the doubling overflowed): scan
662                    // linearly from 1 for the first border, as PUC's
663                    // unbound_search does — finds a small border fast instead of
664                    // returning the huge one.
665                    let mut i = 1i64;
666                    while !self.get_int(i).is_nil() {
667                        i += 1;
668                    }
669                    return i - 1;
670                }
671            }
672        }
673        while hi - lo > 1 {
674            let m = lo + (hi - lo) / 2;
675            if self.get_int(m).is_nil() {
676                hi = m;
677            } else {
678                lo = m;
679            }
680        }
681        lo
682    }
683
684    /// Lua `next`: iterate array part then hash part.
685    pub fn next(&self, key: Value) -> Result<Option<(Value, Value)>, TableError> {
686        let start = match key {
687            Value::Nil => 0,
688            k => {
689                let k = match k {
690                    Value::Float(f) => match f2i_exact(f) {
691                        Some(i) => Value::Int(i),
692                        None => k,
693                    },
694                    k => k,
695                };
696                if let Value::Int(i) = k
697                    && i >= 1
698                    && (i as u64) <= self.asize() as u64
699                {
700                    i as usize
701                } else {
702                    match self.find_node(k) {
703                        Some(idx) => self.asize() + idx + 1,
704                        None => return Err(TableError::InvalidNext),
705                    }
706                }
707            }
708        };
709        let atags = self.atags();
710        for i in start..self.asize() {
711            if atags[i] != raw::NIL {
712                return Ok(Some((Value::Int(i as i64 + 1), self.aget(i))));
713            }
714        }
715        let hstart = start.saturating_sub(self.asize());
716        for (idx, n) in self.nodes.iter().enumerate().skip(hstart) {
717            if !n.val.is_nil() {
718                let _ = idx;
719                return Ok(Some((n.key, n.val)));
720            }
721        }
722        Ok(None)
723    }
724
725    /// `(weak_keys, weak_values)` from the metatable's `__mode` field. Read by
726    /// scanning the metatable for the `__mode` string (no interned key needed
727    /// inside the collector).
728    pub(crate) fn weak_mode(&self) -> (bool, bool) {
729        let Some(mt) = self.metatable else {
730            return (false, false);
731        };
732        for n in mt.nodes.iter() {
733            if let (Value::Str(k), Value::Str(mode)) = (n.key, n.val)
734                && k.as_bytes() == b"__mode"
735            {
736                let b = mode.as_bytes();
737                return (b.contains(&b'k'), b.contains(&b'v'));
738            }
739        }
740        (false, false)
741    }
742
743    /// True when this table holds at least one direct reference (array slot,
744    /// hash key, or hash value) to a coroutine whose mark bit is still clear.
745    /// Used by the GC's cycle-finalize check (PUC 5.3 gc.lua :502) to detect
746    /// the table ↔ thread reference cycle that needs an extra GC round before
747    /// `__gc` runs. Tag-level scan avoids walking the full reference graph.
748    pub(crate) fn refs_contain_unmarked_coro(&self) -> bool {
749        use crate::runtime::heap::header_is_marked;
750        let atags = self.atags();
751        let avals = self.avals();
752        for (i, &tag) in atags.iter().enumerate() {
753            if tag == raw::CORO {
754                // SAFETY: raw union access — the tag byte at the same index in `atags` was previously confirmed to be `co` (closure/object pointer) so the `co` variant of `RawVal` holds the valid payload.
755                let p = unsafe { avals[i].co } as *mut crate::runtime::heap::GcHeader;
756                if !header_is_marked(p) {
757                    return true;
758                }
759            }
760        }
761        for n in self.nodes.iter() {
762            if let Value::Coro(co) = n.key {
763                if !header_is_marked(co.as_ptr() as *mut crate::runtime::heap::GcHeader) {
764                    return true;
765                }
766            }
767            if let Value::Coro(co) = n.val {
768                if !header_is_marked(co.as_ptr() as *mut crate::runtime::heap::GcHeader) {
769                    return true;
770                }
771            }
772        }
773        false
774    }
775
776    pub(crate) fn trace(&self, m: &mut Marker) {
777        let (wk, wv) = self.weak_mode();
778        if wk || wv {
779            m.weak.push(self as *const Table as *mut Table);
780        }
781        // weak keys + strong values = an ephemeron table: its hash values are
782        // marked only if the key proves reachable (deferred to the convergence
783        // pass), not here. PUC 5.1 predates ephemerons — under `no_ephemeron`
784        // a weak-key table marks its values strongly during this pass, which
785        // is what gc.lua's "weak tables" section requires.
786        let ephemeron = wk && !wv && !m.no_ephemeron;
787        if ephemeron {
788            m.ephemeron.push(self as *const Table as *mut Table);
789        }
790        // array keys are integers (never weakly collected); skip values only
791        // when the table has weak values
792        if !wv {
793            let atags = self.atags();
794            let avals = self.avals();
795            for (i, &tag) in atags.iter().enumerate() {
796                if raw::is_gc(tag) {
797                    // SAFETY: `tag` and the raw value come from this table's parallel `atags` / `avals` arrays, which the table writers always keep in sync — the tag byte matches the raw payload's discriminator (see `runtime::value` `raw` module).
798                    m.value(unsafe { Value::pack(tag, avals[i]) });
799                }
800            }
801        }
802        for n in self.nodes.iter() {
803            if !wk {
804                m.value(n.key);
805            }
806            // ephemeron hash values are deferred; otherwise mark strong values
807            if !wv && !ephemeron {
808                m.value(n.val);
809            }
810        }
811        if let Some(mt) = self.metatable {
812            m.value(Value::Table(mt));
813        }
814    }
815
816    /// Ephemeron pass: mark the value of every hash entry whose key is alive
817    /// (`alive` decides — strong/marked keys, plus strings/numbers which are
818    /// never weakly collected). Returns true if any value was newly marked, so
819    /// the caller can iterate to a fixpoint (PUC `traverseephemeron`).
820    pub(crate) fn converge_ephemeron(&self, alive: &dyn Fn(Value) -> bool, m: &mut Marker) -> bool {
821        let mut changed = false;
822        for n in self.nodes.iter() {
823            if !n.val.is_nil() && alive(n.key) {
824                changed |= m.value(n.val);
825            }
826        }
827        changed
828    }
829
830    /// Clear entries whose weak key/value did not survive marking. `is_dead`
831    /// reports whether a GC value was left unmarked (about to be swept).
832    /// Clear weak-table entries whose key/value no longer carries a live
833    /// reference. `is_dead` is a **pure** check (no side effects); the GC
834    /// uses `mark_string` to resurrect any string that's still reachable via
835    /// a *surviving* entry — Lua manual §2.5.4 says strings in weak tables
836    /// are not collected as long as their entry is, and PUC `iscleared`
837    /// implements that by marking the string during the same scan.
838    pub(crate) fn clear_weak(
839        &mut self,
840        wk: bool,
841        wv: bool,
842        is_dead: &dyn Fn(Value) -> bool,
843        mark_string: &dyn Fn(Value),
844    ) {
845        if wv {
846            let n = self.asize as usize;
847            for i in 0..n {
848                let tag = self.atags()[i];
849                if raw::is_gc(tag) {
850                    // SAFETY: `tag` and the raw value come from this table's parallel `atags` / `avals` arrays, which the table writers always keep in sync — the tag byte matches the raw payload's discriminator (see `runtime::value` `raw` module).
851                    let v = unsafe { Value::pack(tag, self.avals()[i]) };
852                    if is_dead(v) {
853                        self.atags_mut()[i] = raw::NIL;
854                        self.avals_mut()[i] = RawVal::NIL;
855                    } else {
856                        mark_string(v);
857                    }
858                }
859            }
860        }
861        for n in self.nodes.iter_mut() {
862            if n.val.is_nil() {
863                continue;
864            }
865            let key_dead = wk && is_dead(n.key);
866            let val_dead = wv && is_dead(n.val);
867            if key_dead || val_dead {
868                // entry removed. PUC `setdeadkey`: when the key was a
869                // collectable, drop the Gc pointer so a later raw_eq cannot
870                // spuriously match a new object that gets allocated at the
871                // same freed address. Keep `next` so the chain back-links
872                // through this node still reach downstream entries; the
873                // `dead_key` flag tells `find_node` to skip the comparison
874                // and `insert_new` to treat the slot as a free
875                // main-position owner that may inherit the chain.
876                n.val = Value::Nil;
877                if matches!(
878                    n.key,
879                    Value::Table(_)
880                        | Value::Closure(_)
881                        | Value::Native(_)
882                        | Value::Coro(_)
883                        | Value::Userdata(_)
884                        | Value::Str(_)
885                ) {
886                    n.key = Value::Nil;
887                    n.dead_key = true;
888                }
889            } else {
890                // entry survives — resurrect any string reachable through it
891                if wk {
892                    mark_string(n.key);
893                }
894                if wv {
895                    mark_string(n.val);
896                }
897            }
898        }
899    }
900}
901
902fn normalize_set_key(key: Value) -> Result<Value, TableError> {
903    match key {
904        Value::Nil => Err(TableError::NilIndex),
905        Value::Float(f) => match f2i_exact(f) {
906            Some(i) => Ok(Value::Int(i)),
907            None if f.is_nan() => Err(TableError::NanIndex),
908            None => Ok(key),
909        },
910        k => Ok(k),
911    }
912}
913
914fn hash_key(k: Value) -> u64 {
915    match k {
916        Value::Int(i) => i as u64, // identity mod size (PUC hashint)
917        Value::Float(f) => mix64(f.to_bits()),
918        Value::Bool(b) => b as u64 + 1,
919        Value::Str(s) => s.hash() as u64,
920        Value::Table(t) => mix64(t.as_ptr() as u64),
921        Value::Closure(c) => mix64(c.as_ptr() as u64),
922        Value::Native(n) => mix64(n.as_ptr() as u64),
923        Value::Coro(co) => mix64(co.as_ptr() as u64),
924        Value::Userdata(u) => mix64(u.as_ptr() as u64),
925        Value::LightUserdata(p) => mix64(p as u64),
926        Value::Nil => 0, // unreachable as a stored key
927    }
928}
929
930/// splitmix64 finalizer.
931fn mix64(mut x: u64) -> u64 {
932    x ^= x >> 30;
933    x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
934    x ^= x >> 27;
935    x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
936    x ^ (x >> 31)
937}
938
939/// For k ≥ 1: the bucket l such that k ∈ (2^(l-1), 2^l].
940fn ceil_log2(k: u64) -> usize {
941    (u64::BITS - (k - 1).leading_zeros()) as usize
942}
943
944impl Table {
945    /// Preallocate the array part (table.create); existing contents are
946    /// preserved.
947    pub fn ensure_array(&mut self, heap: &mut Heap, n: usize) {
948        if n > self.asize() {
949            let hash_entries = self.nodes.iter().filter(|nd| !nd.val.is_nil()).count();
950            self.resize(heap, n, hash_entries);
951        }
952    }
953}
954
955impl Table {
956    /// Preallocate hash-part capacity (table.create's second size).
957    pub fn ensure_hash(&mut self, heap: &mut Heap, n: usize) {
958        let entries = self.nodes.iter().filter(|nd| !nd.val.is_nil()).count();
959        if n > self.nodes.len() {
960            self.resize(heap, self.asize(), n.max(entries));
961        }
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use super::*;
968    use crate::runtime::heap::Heap;
969
970    fn with_table(f: impl FnOnce(&mut Heap, &mut Table)) {
971        let mut heap = Heap::new();
972        let t = heap.new_table();
973        f(&mut heap, unsafe { t.as_mut() });
974    }
975
976    fn assert_is_border(t: &Table, n: i64) {
977        if n == 0 {
978            assert!(t.get_int(1).is_nil(), "border 0 but t[1] non-nil");
979        } else {
980            assert!(!t.get_int(n).is_nil(), "border {n} but t[{n}] is nil");
981            assert!(
982                t.get_int(n + 1).is_nil(),
983                "border {n} but t[{}] non-nil",
984                n + 1
985            );
986        }
987    }
988
989    #[test]
990    fn sequence_grows_into_array() {
991        with_table(|heap, t| {
992            for i in 1..=1000 {
993                let _ = t.set_int(heap, i, Value::Int(i * 10));
994            }
995            for i in 1..=1000 {
996                assert!(t.get_int(i).raw_eq(Value::Int(i * 10)));
997            }
998            assert_eq!(t.len(), 1000);
999        });
1000    }
1001
1002    #[test]
1003    fn string_and_mixed_keys() {
1004        with_table(|heap, t| {
1005            let k1 = Value::Str(heap.intern(b"alpha"));
1006            let k2 = Value::Str(heap.intern(b"beta"));
1007            t.set(heap, k1, Value::Int(1)).unwrap();
1008            t.set(heap, k2, Value::Int(2)).unwrap();
1009            t.set(heap, Value::Bool(true), Value::Int(3)).unwrap();
1010            t.set(heap, Value::Int(-5), Value::Int(4)).unwrap();
1011            // re-interned key reaches the same slot
1012            let k1b = Value::Str(heap.intern(b"alpha"));
1013            assert!(t.get(k1b).raw_eq(Value::Int(1)));
1014            assert!(t.get(k2).raw_eq(Value::Int(2)));
1015            assert!(t.get(Value::Bool(true)).raw_eq(Value::Int(3)));
1016            assert!(t.get(Value::Int(-5)).raw_eq(Value::Int(4)));
1017            assert!(t.get(Value::Str(heap.intern(b"gamma"))).is_nil());
1018        });
1019    }
1020
1021    #[test]
1022    fn float_keys_normalize_to_int() {
1023        with_table(|heap, t| {
1024            t.set(heap, Value::Float(2.0), Value::Int(22)).unwrap();
1025            assert!(t.get(Value::Int(2)).raw_eq(Value::Int(22)));
1026            t.set(heap, Value::Int(3), Value::Int(33)).unwrap();
1027            assert!(t.get(Value::Float(3.0)).raw_eq(Value::Int(33)));
1028            // -0.0 is key 0
1029            t.set(heap, Value::Float(-0.0), Value::Int(0)).unwrap();
1030            assert!(t.get(Value::Int(0)).raw_eq(Value::Int(0)));
1031            // non-integral floats are their own keys
1032            t.set(heap, Value::Float(0.5), Value::Int(55)).unwrap();
1033            assert!(t.get(Value::Float(0.5)).raw_eq(Value::Int(55)));
1034            assert!(t.get(Value::Int(0)).raw_eq(Value::Int(0)));
1035        });
1036    }
1037
1038    #[test]
1039    fn bad_keys() {
1040        with_table(|heap, t| {
1041            assert_eq!(
1042                t.set(heap, Value::Nil, Value::Int(1)),
1043                Err(TableError::NilIndex)
1044            );
1045            assert_eq!(
1046                t.set(heap, Value::Float(f64::NAN), Value::Int(1)),
1047                Err(TableError::NanIndex)
1048            );
1049            // reads with bad keys are nil, not errors
1050            assert!(t.get(Value::Nil).is_nil());
1051            assert!(t.get(Value::Float(f64::NAN)).is_nil());
1052        });
1053    }
1054
1055    #[test]
1056    fn delete_and_reinsert() {
1057        with_table(|heap, t| {
1058            let k = Value::Str(heap.intern(b"k"));
1059            t.set(heap, k, Value::Int(1)).unwrap();
1060            t.set(heap, k, Value::Nil).unwrap();
1061            assert!(t.get(k).is_nil());
1062            t.set(heap, k, Value::Int(2)).unwrap();
1063            assert!(t.get(k).raw_eq(Value::Int(2)));
1064            // setting an absent key to nil stays absent
1065            let k2 = Value::Str(heap.intern(b"k2"));
1066            t.set(heap, k2, Value::Nil).unwrap();
1067            assert!(t.get(k2).is_nil());
1068        });
1069    }
1070
1071    #[test]
1072    fn borders_with_holes() {
1073        with_table(|heap, t| {
1074            let _ = t.set_int(heap, 1, Value::Int(1));
1075            let _ = t.set_int(heap, 2, Value::Int(2));
1076            assert_eq!(t.len(), 2);
1077            t.set_int(heap, 2, Value::Nil).unwrap();
1078            assert_is_border(t, t.len());
1079            // hash-resident tail
1080            let _ = t.set_int(heap, 1_000_000, Value::Int(1));
1081            assert_is_border(t, t.len());
1082        });
1083    }
1084
1085    #[test]
1086    fn len_on_empty_and_hash_only() {
1087        with_table(|heap, t| {
1088            assert_eq!(t.len(), 0);
1089            let xk = Value::Str(heap.intern(b"x"));
1090            t.set(heap, xk, Value::Int(1)).unwrap();
1091            assert_eq!(t.len(), 0);
1092        });
1093    }
1094
1095    #[test]
1096    fn next_iterates_everything_exactly_once() {
1097        with_table(|heap, t| {
1098            let mut expected = 0i64;
1099            for i in 1..=64 {
1100                let _ = t.set_int(heap, i, Value::Int(i));
1101                expected += i;
1102            }
1103            for i in 0..32 {
1104                let k = Value::Str(heap.intern(format!("s{i}").as_bytes()));
1105                t.set(heap, k, Value::Int(1000 + i)).unwrap();
1106                expected += 1000 + i;
1107            }
1108            t.set(heap, Value::Float(2.5), Value::Int(7)).unwrap();
1109            expected += 7;
1110
1111            let mut sum = 0i64;
1112            let mut count = 0;
1113            let mut key = Value::Nil;
1114            while let Some((k, v)) = t.next(key).unwrap() {
1115                let Value::Int(x) = v else {
1116                    panic!("bad value")
1117                };
1118                sum += x;
1119                count += 1;
1120                key = k;
1121            }
1122            assert_eq!(count, 64 + 32 + 1);
1123            assert_eq!(sum, expected);
1124        });
1125    }
1126
1127    #[test]
1128    fn next_skips_nil_values_and_rejects_alien_keys() {
1129        with_table(|heap, t| {
1130            let _ = t.set_int(heap, 1, Value::Int(1));
1131            let _ = t.set_int(heap, 3, Value::Int(3));
1132            let k = Value::Str(heap.intern(b"gone"));
1133            t.set(heap, k, Value::Int(9)).unwrap();
1134            t.set(heap, k, Value::Nil).unwrap();
1135            let mut seen = Vec::new();
1136            let mut key = Value::Nil;
1137            while let Some((k, v)) = t.next(key).unwrap() {
1138                let Value::Int(x) = v else { panic!() };
1139                seen.push(x);
1140                key = k;
1141            }
1142            assert_eq!(seen, vec![1, 3]);
1143            // a key never inserted is invalid for next
1144            let alien = Value::Str(heap.intern(b"never"));
1145            assert!(matches!(t.next(alien), Err(TableError::InvalidNext)));
1146            // ...but a deleted (nil-valued) key is still a valid cursor
1147            assert!(t.next(k).is_ok());
1148        });
1149    }
1150
1151    #[test]
1152    fn collision_relocation_keeps_chains_intact() {
1153        with_table(|heap, t| {
1154            // dense negative ints all land in the hash part; with identity
1155            // hashing they exercise both chain cases heavily
1156            for i in 0..512 {
1157                let _ = t.set_int(heap, -i, Value::Int(i));
1158            }
1159            for i in 0..512 {
1160                assert!(t.get_int(-i).raw_eq(Value::Int(i)), "lost key {}", -i);
1161            }
1162        });
1163    }
1164
1165    #[test]
1166    fn rehash_redistributes_into_array() {
1167        with_table(|heap, t| {
1168            // insert 1..n in reverse: starts in hash, rehash must migrate
1169            for i in (1..=256).rev() {
1170                let _ = t.set_int(heap, i, Value::Int(i));
1171            }
1172            assert_eq!(t.len(), 256);
1173            for i in 1..=256 {
1174                assert!(t.get_int(i).raw_eq(Value::Int(i)));
1175            }
1176        });
1177    }
1178}