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}