luna_core/runtime/heap.rs
1//! GC heap v1: precise stop-the-world mark & sweep over an intrusive
2//! all-objects list (PUC `allgc` shape). All unsafe object plumbing is
3//! confined to this module and `string`/`table` internals.
4//!
5//! `Gc<T>` safety contract: the runtime is single-threaded; a `Gc` pointer is
6//! valid until a `collect()` call that does not reach it from the given
7//! roots. Callers must root every value they keep across a collect.
8
9use std::fmt;
10use std::ops::Deref;
11use std::ptr::{self, NonNull};
12
13use crate::runtime::function::{LuaClosure, NativeClosure, Proto, UpvalState, Upvalue};
14use crate::runtime::string::{self, LuaStr, StringTable};
15use crate::runtime::table::Table;
16use crate::runtime::userdata::{Userdata, UserdataPayload};
17use crate::runtime::value::Value;
18
19/// Discriminator the GC stores in every [`GcHeader`] so a raw header pointer
20/// can be cast back to the right object kind during tracing and sweeping.
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22#[repr(u8)]
23pub enum ObjTag {
24 /// [`crate::runtime::string::LuaStr`].
25 Str,
26 /// [`crate::runtime::table::Table`].
27 Table,
28 /// [`crate::runtime::function::Proto`].
29 Proto,
30 /// [`crate::runtime::function::LuaClosure`].
31 Closure,
32 /// [`crate::runtime::function::Upvalue`].
33 Upvalue,
34 /// [`crate::runtime::function::NativeClosure`].
35 Native,
36 /// [`crate::runtime::coroutine::Coro`].
37 Coro,
38 /// [`crate::runtime::userdata::Userdata`].
39 Userdata,
40}
41
42/// Header prefix on every GC-managed object: intrusive next-link + type tag +
43/// mark bits. Always at offset 0 of the containing struct (`#[repr(C)]`).
44#[repr(C)]
45pub struct GcHeader {
46 next: *mut GcHeader,
47 tag: ObjTag,
48 /// tricolor + finalizer state. PUC `gch.marked` layout (lgc.h):
49 /// bit 0 WHITE0 — current-white-A
50 /// bit 1 WHITE1 — current-white-B (the unused white in any given cycle
51 /// is the "other-white" / dead-white at sweep time)
52 /// bit 2 BLACK — propagated; outgoing refs already traced
53 /// bit 3 FIN — registered for `__gc` (tracked in `finalize`)
54 /// bit 4 FINALIZED — already enqueued or finalized once this lifetime
55 /// bit 5 DEFERRED — 5.3 cycle-finalize deferral marker (gc.lua :502)
56 /// Gray = no white bits, no BLACK; that is the in-stack state between the
57 /// time a Marker visits an object and the time it traces it.
58 flags: u8,
59}
60
61const WHITE0: u8 = 1;
62const WHITE1: u8 = 2;
63const BLACK: u8 = 4;
64const WHITE_BITS: u8 = WHITE0 | WHITE1;
65const COLOR_BITS: u8 = WHITE_BITS | BLACK;
66
67/// registered for finalization (`__gc`): the object is tracked in `finalize`.
68const FIN: u8 = 8;
69/// finalization already scheduled/run: never finalize this object again (PUC
70/// FINALIZEDBIT). Set when it moves to `tobefnz`.
71const FINALIZED: u8 = 16;
72/// resurrected once because a reference cycle through a coroutine kept the
73/// finalizable alive (PUC 5.3 gc.lua :502 "two collections are needed to
74/// break cycle"). The next time the object is found unreachable it is moved
75/// to `tobefnz` without re-deferring.
76const DEFERRED: u8 = 32;
77
78#[inline(always)]
79fn is_white(flags: u8) -> bool {
80 flags & WHITE_BITS != 0
81}
82#[inline(always)]
83fn is_black(flags: u8) -> bool {
84 flags & BLACK != 0
85}
86
87/// True when an object header has been reached by marking (gray or black).
88/// `pub(crate)` so other runtime modules (e.g. `Table::refs_contain_unmarked_coro`)
89/// can probe reachability without owning the bit constants. Equivalent to
90/// `isgray(o) || isblack(o)` in PUC.
91pub(crate) fn header_is_marked(h: *mut GcHeader) -> bool {
92 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
93 unsafe { !is_white((*h).flags) }
94}
95
96impl GcHeader {
97 pub(crate) fn new(tag: ObjTag) -> GcHeader {
98 GcHeader {
99 next: ptr::null_mut(),
100 tag,
101 flags: 0,
102 }
103 }
104}
105
106/// `Copy` handle to a heap-allocated GC-managed object. Layout is a single
107/// `NonNull<T>`; the GC walks reachability via root scanning and intrusive
108/// linkage on [`GcHeader`], not via reference counts.
109pub struct Gc<T> {
110 ptr: NonNull<T>,
111}
112
113impl<T> Clone for Gc<T> {
114 fn clone(&self) -> Self {
115 *self
116 }
117}
118impl<T> Copy for Gc<T> {}
119
120impl<T> Gc<T> {
121 #[doc(hidden)]
122 pub fn from_ptr(p: *mut T) -> Gc<T> {
123 Gc {
124 ptr: NonNull::new(p).expect("gc pointer must be non-null"),
125 }
126 }
127
128 /// Raw pointer to the referent. Always non-null; valid for the lifetime
129 /// of the [`Heap`] that allocated it as long as the object is reachable.
130 pub fn as_ptr(self) -> *mut T {
131 self.ptr.as_ptr()
132 }
133
134 /// Pointer-identity equality (PUC `rawequal` for reference types).
135 pub fn ptr_eq(self, other: Gc<T>) -> bool {
136 self.ptr == other.ptr
137 }
138
139 /// SAFETY: caller must ensure no other live reference to the object and
140 /// no collect() while the borrow is held (single-threaded runtime).
141 ///
142 /// `#[doc(hidden)]` so the documented public surface needs no `unsafe`:
143 /// embedders should not see this in rustdoc. The safe path for mutating
144 /// freshly-allocated tables is the `TableBuilder` / `vm.table_of(...)` API.
145 /// Cross-crate access from `luna` (e.g. `jit_backend`, `capi`) keeps
146 /// working — `#[doc(hidden)] pub` doesn't demote visibility, just docs.
147 #[doc(hidden)]
148 pub unsafe fn as_mut<'a>(self) -> &'a mut T {
149 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
150 unsafe { &mut *self.ptr.as_ptr() }
151 }
152}
153
154impl<T> Deref for Gc<T> {
155 type Target = T;
156 fn deref(&self) -> &T {
157 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
158 unsafe { self.ptr.as_ref() }
159 }
160}
161
162impl<T> fmt::Debug for Gc<T> {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "Gc({:p})", self.ptr.as_ptr())
165 }
166}
167
168/// Incremental GC phase.
169/// * `Pause` — no cycle in progress; all objects current-white.
170/// * `Propagate` — gray queue + propagate-state populated; mutator runs
171/// alongside `gc_step_propagate(budget)` calls. Born objects
172/// stamp the current-white; barriers re-gray modified
173/// parents. Transitions to `Sweep` via `gc_finish_atomic`.
174/// * `Sweep` — `sweep_cur` is the detached old heap being budget-swept.
175///
176/// `mark_all` (the STW path used by `collect_ex`) sequences start_propagate +
177/// drain_all + finish_atomic inline, never crossing a step boundary.
178#[derive(Clone, Copy, PartialEq, Eq, Debug)]
179enum GcPhase {
180 Pause,
181 Propagate,
182 Sweep,
183}
184
185/// Cross-step traversal state for the incremental Propagate phase. Owned by
186/// `Heap.propagate` (`Some` between `gc_start_propagate` and `gc_finish_atomic`,
187/// `None` otherwise). The gray queue itself lives in `Heap.gray` so write
188/// barriers can push directly without going through the Option.
189struct PropagateState {
190 weak: Vec<*mut Table>,
191 ephemeron: Vec<*mut Table>,
192 cached_protos: Vec<*mut Proto>,
193 no_ephemeron: bool,
194}
195
196/// luna's incremental mark-sweep GC heap. Owns every [`Gc<T>`] allocation
197/// (one per Vm); produces handles via the `new_*` constructors and traces
198/// reachability through registered roots. Holds the string-intern table and
199/// the auto-GC pacing state.
200pub struct Heap {
201 all: *mut GcHeader,
202 strings: StringTable,
203 seed: u32,
204 live: usize,
205 /// approximate allocated bytes — shells only. Each `link()` adds
206 /// `size_of::<T>` for its tag; each `free_obj` subtracts the same.
207 /// Internal Vec/Box growth (table array/hash parts, proto code,
208 /// closure upvals slice) is NOT auto-tracked, so this is a lower
209 /// bound on real memory. PUC's `g->GCtotalbytes` is exact because
210 /// `lmem.c` routes every malloc/free through one helper; luna pays
211 /// for that uniformity in exchange for a smaller, drift-free count.
212 bytes: usize,
213 /// byte threshold at which the VM should run a collection (auto-GC pacing)
214 next_gc: usize,
215 /// PUC `g->currentwhite`: which white bit (WHITE0 or WHITE1) means
216 /// "born / surviving this cycle". The other white is the dead-white that
217 /// sweep collects. Flipped at the end of each mark cycle (`atomic`).
218 current_white: u8,
219 /// Persistent gray queue: holds objects grayed by write barriers between
220 /// the time the marker first reached them and the next propagate step.
221 /// Lives outside `propagate` so barriers can push without going through
222 /// the Option; `gc_step_propagate` and `gc_finish_atomic` drain it.
223 gray: Vec<*mut GcHeader>,
224 /// Incremental traversal state. `Some` between `gc_start_propagate` and
225 /// `gc_finish_atomic` (and inline within `mark_all`); `None` otherwise.
226 propagate: Option<PropagateState>,
227 /// incremental-sweep phase (Pause unless a `step` cycle is mid-sweep)
228 phase: GcPhase,
229 /// the remaining detached object list being swept during `GcPhase::Sweep`;
230 /// survivors are spliced back onto `all`, garbage is freed
231 sweep_cur: *mut GcHeader,
232 /// `collectgarbage("stop")`: auto-GC is suspended while true
233 gc_stopped: bool,
234 /// objects registered for finalization (a live `__gc` metamethod was set);
235 /// parallel-tracked — ownership stays on `all` (PUC `finobj`).
236 finalize: Vec<*mut GcHeader>,
237 /// dead finalizables resurrected this cycle, awaiting their `__gc` call by
238 /// the VM (PUC `tobefnz`). Drained via `take_tobefnz`.
239 tobefnz: Vec<*mut GcHeader>,
240 /// PUC 5.1 has no ephemeron pass: a `__mode='k'` table marks its values
241 /// strongly during traversal, so entries like `a[t]=t` (key and value the
242 /// same fresh object) survive even with nothing else referencing `t`.
243 /// 5.2+ replaced that with ephemeron convergence. gc.lua's "weak tables"
244 /// section in 5.1 asserts 3*lim survivors, 5.4 only 2*lim — the loop2
245 /// pair was retired from the newer test as a result.
246 pub(crate) no_ephemeron: bool,
247 /// PUC 5.3 finalizes a table caught in a cycle through an unreachable
248 /// coroutine one GC round later than the unreachability is detected
249 /// ("two collections are needed to break cycle", gc.lua :502). 5.4 and 5.5
250 /// rewrote the GC and finalize the same cycle in a single pass (their
251 /// gc.lua :544 asserts collected after one `collectgarbage()`). 5.1/5.2
252 /// don't exercise this path. Set by the VM at construction.
253 pub(crate) defer_thread_cycle_finalize: bool,
254 /// Pool of freed Table allocations.
255 /// btrees-style workloads create + free ~32k tables per iter;
256 /// jemalloc's malloc/free roundtrip costs ~30ns per table = ~960µs
257 /// total per iter. Pool recycle: free_obj pushes the raw Table
258 /// pointer here instead of dropping; new_table pops + resets fields.
259 /// Cap at 4096 entries to avoid unbounded growth (worst-case: 4096
260 /// × sizeof(Table) ≈ 460 KB resident memory in idle pool).
261 table_pool: Vec<std::ptr::NonNull<crate::runtime::table::Table>>,
262 /// `gc-verify` — headers freed since the last collect
263 /// began. O(1) read-time dangling probes (`Vm::op_index`) test
264 /// membership here; cleared when the next mark starts. Only exact
265 /// under ASAN-style quarantining allocators (no immediate reuse).
266 #[cfg(feature = "gc-verify")]
267 pub(crate) recently_freed: std::collections::HashSet<usize>,
268 /// Embedding memory cap. When `Some(n)`, the VM's run loop watches
269 /// `bytes` between dispatch turns and, on overshoot, runs a full collect
270 /// and (still overshooting) raises a catchable "memory cap exceeded"
271 /// Lua error. A soft cap, not a hard alloc-time refusal: a single
272 /// allocation can briefly push `bytes` past `n`, but the embedder gets
273 /// control back at the next safe point — host policy.
274 pub(crate) mem_cap: Option<usize>,
275}
276
277/// Initial auto-GC threshold and floor (PUC GCSTEPSIZE-ish pacing).
278const GC_MIN_THRESHOLD: usize = 1 << 20;
279
280impl Heap {
281 /// Build a fresh empty heap with default GC pacing and no memory cap.
282 pub fn new() -> Heap {
283 Heap {
284 all: ptr::null_mut(),
285 strings: StringTable::new(),
286 seed: make_seed(),
287 live: 0,
288 bytes: 0,
289 next_gc: GC_MIN_THRESHOLD,
290 current_white: WHITE0,
291 gray: Vec::new(),
292 propagate: None,
293 phase: GcPhase::Pause,
294 sweep_cur: ptr::null_mut(),
295 gc_stopped: false,
296 finalize: Vec::new(),
297 tobefnz: Vec::new(),
298 no_ephemeron: false,
299 defer_thread_cycle_finalize: false,
300 mem_cap: None,
301 table_pool: Vec::new(),
302 #[cfg(feature = "gc-verify")]
303 recently_freed: std::collections::HashSet::new(),
304 }
305 }
306
307 fn link(&mut self, h: *mut GcHeader) {
308 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
309 unsafe {
310 (*h).next = self.all;
311 // Born color depends on phase:
312 // * Pause / Sweep — born current-white (PUC `luaC_white(g)`);
313 // reachable from roots gets marked next cycle.
314 // * Propagate — born BLACK (PUC `LUAGCRYOUNG` / sasimpl
315 // of new-during-cycle). Born-current-white during Propagate
316 // would lose the WHITE bits at the upcoming atomic flip and
317 // be swept this same cycle even when reachable from a
318 // barrier-grayed root. Born BLACK skips the trace and
319 // transitions to current-white at sweep, matching the
320 // reachable-survivor flow.
321 let born = if self.phase == GcPhase::Propagate {
322 BLACK
323 } else {
324 self.current_white
325 };
326 (*h).flags = ((*h).flags & !COLOR_BITS) | born;
327 }
328 self.all = h;
329 self.live += 1;
330 }
331
332 /// Take ownership of a boxed object and put it under GC management.
333 /// SAFETY-by-convention: `T` must be `repr(C)` with a `GcHeader` first
334 /// field whose tag matches `T` (enforced by the typed constructors).
335 pub(crate) fn adopt<T>(&mut self, obj: Box<T>) -> Gc<T> {
336 let p = Box::into_raw(obj);
337 self.link(p as *mut GcHeader);
338 self.bytes += std::mem::size_of::<T>();
339 Gc::from_ptr(p)
340 }
341
342 /// Allocate and adopt a fresh empty [`Table`].
343 pub fn new_table(&mut self) -> Gc<Table> {
344 // table_pool fast path. When btrees-
345 // style alloc bursts have left freed Tables in the pool, pop a
346 // recycled one and reset its fields instead of mallocing fresh.
347 // Saves ~30ns per alloc (malloc roundtrip elided).
348 let p = if let Some(ptr) = self.table_pool.pop() {
349 let t = ptr.as_ptr();
350 // gc-verify: the pointer is alive again — drop it from the
351 // freed log so read-time probes don't flag the recycled table.
352 #[cfg(feature = "gc-verify")]
353 {
354 self.recently_freed.remove(&(t as usize));
355 crate::runtime::gc_verify_probe::FREED
356 .with(|f| f.borrow_mut().remove(&(t as usize)));
357 }
358 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
359 unsafe {
360 // Reset to fresh-Table state. Box-owned slab/nodes/
361 // metatable were already cleared in `free_obj` before
362 // pool push, so we only reset stack-resident fields here.
363 (*t).hdr = GcHeader::new(ObjTag::Table);
364 (*t).array_ptr = std::ptr::null_mut();
365 (*t).asize = 0;
366 (*t).inline_storage =
367 std::cell::UnsafeCell::new([0; crate::runtime::table::INLINE_U64S]);
368 (*t).lastfree = 0;
369 (*t).flags = 0;
370 }
371 t
372 } else {
373 Box::into_raw(Box::new(Table::new(GcHeader::new(ObjTag::Table))))
374 };
375 // Link + bytes accounting (same as adopt path).
376 self.link(p as *mut GcHeader);
377 self.bytes += std::mem::size_of::<Table>();
378 let g = Gc::from_ptr(p);
379 // the Table is now at its final heap address; wire
380 // `array_ptr` to point at the inline storage that lives inside
381 // the boxed Table.
382 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
383 unsafe { g.as_mut() }.init_array_ptr();
384 g
385 }
386
387 /// Adopt an empty table and pre-allocate `asize`
388 /// NIL slots in the array part. Equivalent to `new_table()`
389 /// followed by `set_int(N, Nil)` worth of `rehash`es, except
390 /// the array reaches its final size in one allocation rather
391 /// than O(log N) doubling rounds.
392 ///
393 /// `asize == 0` is identical to `new_table()`. Larger sizes
394 /// are clamped at the array part's hard ceiling
395 /// `MAX_ASIZE = 2^27`; requests beyond that fall back to the
396 /// empty table, which the interpreter would have grown
397 /// gracefully via rehash anyway.
398 pub fn new_table_sized(&mut self, asize: usize) -> Gc<Table> {
399 const MAX_ASIZE_HINT: usize = 1 << 27;
400 let g = self.new_table();
401 let clamped = asize.min(MAX_ASIZE_HINT);
402 if clamped > 0 {
403 // SAFETY: the freshly adopted table has no live borrow
404 // anywhere else; we hold the only `Gc<Table>` handle.
405 unsafe { g.as_mut() }.resize(self, clamped, 0);
406 }
407 g
408 }
409
410 /// Adopt a compiler-built prototype (its `hdr` must carry ObjTag::Proto).
411 pub fn adopt_proto(&mut self, proto: Proto) -> Gc<Proto> {
412 debug_assert!(proto.hdr.tag == ObjTag::Proto);
413 self.adopt(Box::new(proto))
414 }
415
416 /// Back-compat constructor for callers that already
417 /// built a `Box<[Gc<Upvalue>]>`. Internally re-routes through
418 /// `new_closure_inline` so small-upval cases also pick the
419 /// inline path (the input Box is freed after the copy).
420 pub fn new_closure(&mut self, proto: Gc<Proto>, upvals: Box<[Gc<Upvalue>]>) -> Gc<LuaClosure> {
421 use crate::runtime::function::INLINE_UPVALS_N;
422 let n = upvals.len();
423 if n <= INLINE_UPVALS_N {
424 let g = self.new_closure_inline(proto, &upvals);
425 drop(upvals);
426 g
427 } else {
428 // Large closure — store the input Box directly in
429 // `overflow`, no copy.
430 self.adopt_closure_with(proto, n as u32, |c| {
431 c.overflow = upvals;
432 })
433 }
434 }
435
436 /// Hot-path constructor for the `Op::Closure` handler.
437 /// Takes a slice (typically backed by a stack array) so the caller
438 /// doesn't allocate a Vec/Box just to hand it over. Upvals are
439 /// copied into `inline_storage` for small closures, or into a
440 /// freshly-allocated `Box<[..]>` for the rare overflow case.
441 pub fn new_closure_inline(
442 &mut self,
443 proto: Gc<Proto>,
444 upvals: &[Gc<Upvalue>],
445 ) -> Gc<LuaClosure> {
446 use crate::runtime::function::INLINE_UPVALS_N;
447 let n = upvals.len();
448 self.adopt_closure_with(proto, n as u32, |c| {
449 if n <= INLINE_UPVALS_N {
450 for (i, &uv) in upvals.iter().enumerate() {
451 // SAFETY: exclusive &mut c inside the constructor;
452 // write through the cell so no direct borrow of
453 // the inline array is formed (see the field doc).
454 unsafe {
455 (*c.inline_storage.get())[i] = std::mem::MaybeUninit::new(uv);
456 }
457 }
458 } else {
459 c.overflow = upvals.to_vec().into_boxed_slice();
460 }
461 })
462 }
463
464 fn adopt_closure_with<F: FnOnce(&mut LuaClosure)>(
465 &mut self,
466 proto: Gc<Proto>,
467 upvals_len: u32,
468 fill: F,
469 ) -> Gc<LuaClosure> {
470 let mut boxed = Box::new(LuaClosure {
471 hdr: GcHeader::new(ObjTag::Closure),
472 proto,
473 upvals_ptr: std::ptr::null_mut(),
474 upvals_len,
475 inline_storage: std::cell::UnsafeCell::new(
476 [std::mem::MaybeUninit::<Gc<Upvalue>>::uninit();
477 crate::runtime::function::INLINE_UPVALS_N],
478 ),
479 overflow: Box::new([]),
480 });
481 // Box is heap-stable now — populate storage at the final
482 // address so `upvals_ptr` will be valid.
483 fill(&mut boxed);
484 let g = self.adopt(boxed);
485 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
486 unsafe { g.as_mut() }.init_upvals_ptr();
487 g
488 }
489
490 /// Allocate a [`NativeClosure`] wrapping host function `f` with the
491 /// given captured upvalues.
492 pub fn new_native(
493 &mut self,
494 f: crate::runtime::value::NativeFn,
495 upvals: Box<[Value]>,
496 ) -> Gc<NativeClosure> {
497 self.adopt(Box::new(NativeClosure {
498 hdr: GcHeader::new(ObjTag::Native),
499 f,
500 upvals,
501 is_async: false,
502 }))
503 }
504
505 /// Like [`Heap::new_native`] but tags the
506 /// closure with `is_async = true`. The dispatcher's native-call
507 /// path then transmutes `f` to `AsyncNativeFn` and routes through
508 /// the cooperative-yield path. The caller is responsible for
509 /// having transmuted the `AsyncNativeFn` pointer to `NativeFn`
510 /// shape (both are `fn` pointers of the same size); see
511 /// [`crate::vm::async_drive`] for the helper that does this.
512 pub fn new_async_native(
513 &mut self,
514 f: crate::runtime::value::NativeFn,
515 upvals: Box<[Value]>,
516 ) -> Gc<NativeClosure> {
517 self.adopt(Box::new(NativeClosure {
518 hdr: GcHeader::new(ObjTag::Native),
519 f,
520 upvals,
521 is_async: true,
522 }))
523 }
524
525 /// Allocate a fresh [`Upvalue`] cell in the given `state` (open / closed).
526 pub fn new_upvalue(&mut self, state: UpvalState) -> Gc<Upvalue> {
527 self.adopt(Box::new(Upvalue {
528 hdr: GcHeader::new(ObjTag::Upvalue),
529 state,
530 }))
531 }
532
533 /// Create a fresh suspended coroutine wrapping `body`. The new thread
534 /// inherits the creator's globals table; a `setfenv(0, env)` inside it
535 /// will retune that copy without affecting the creator.
536 pub fn new_coro(
537 &mut self,
538 body: Value,
539 globals: Gc<crate::runtime::Table>,
540 ) -> Gc<crate::runtime::Coro> {
541 self.adopt(Box::new(crate::runtime::Coro {
542 hdr: GcHeader::new(ObjTag::Coro),
543 status: crate::runtime::CoroStatus::Suspended,
544 body,
545 started: false,
546 resumer: None,
547 resume_at: None,
548 error_value: None,
549 error_traceback: None,
550 error_levels: None,
551 natives: 0..0,
552 stack: Vec::new(),
553 frames: Vec::new(),
554 frame_ccmt: Vec::new(),
555 open_upvals: Vec::new(),
556 tbc: Vec::new(),
557 top: 0,
558 pcall_depth: 0,
559 hook: crate::vm::exec::HookState::default(),
560 globals,
561 }))
562 }
563
564 /// Create a userdata (an io file handle — luna's only userdata) with no
565 /// metatable yet; the io library installs the shared `FILE*` metatable.
566 pub fn new_userdata(&mut self, payload: UserdataPayload, writable: bool) -> Gc<Userdata> {
567 self.adopt(Box::new(Userdata::new(
568 GcHeader::new(ObjTag::Userdata),
569 payload,
570 writable,
571 )))
572 }
573
574 /// Create (or find) a string. Short strings (≤ 40 bytes) are interned.
575 pub fn intern(&mut self, bytes: &[u8]) -> Gc<LuaStr> {
576 if bytes.len() <= string::MAX_SHORT_LEN {
577 let (p, is_new) = self.strings.intern(bytes, self.seed);
578 if is_new {
579 self.link(p as *mut GcHeader);
580 self.bytes += string::alloc_size(bytes.len());
581 } else {
582 // PUC `luaS_new` resurrect guard (lstring.c).
583 // The bucket-chain is walked open-loop without consulting GC
584 // color; during incremental sweep an existing entry may be
585 // dead-white (in `sweep_cur`, scheduled for `free_obj`). If we
586 // hand its pointer back, the budget-paced sweep frees it out
587 // from under the mutator and the next bucket walk dereferences
588 // a libc-recycled slot — the symptom recorded in
589 // `0x800002a80000002d` deep in `StringTable::intern`).
590 //
591 // Flip the white bits to `current_white` so the upcoming sweep
592 // skips it (PUC `changewhite`). Black / not-white objects are
593 // already safe and untouched.
594 // SAFETY: `p` came from `StringTable::intern` and is a valid
595 // `LuaStr` header (its bucket chain is consistent under our
596 // single-threaded heap).
597 unsafe {
598 let f = (*(p as *mut GcHeader)).flags;
599 if is_white(f) && (f & self.current_white) == 0 {
600 (*(p as *mut GcHeader)).flags = (f & !WHITE_BITS) | self.current_white;
601 }
602 }
603 }
604 Gc::from_ptr(p)
605 } else {
606 let p = string::alloc_long(bytes, self.seed);
607 self.link(p as *mut GcHeader);
608 self.bytes += string::alloc_size(bytes.len());
609 Gc::from_ptr(p)
610 }
611 }
612
613 /// Number of GC-managed objects currently linked into the heap (live + not
614 /// yet swept). Useful for `collectgarbage("count")`-style introspection.
615 pub fn live_objects(&self) -> usize {
616 self.live
617 }
618
619 /// Approximate heap size in bytes.
620 pub fn bytes(&self) -> usize {
621 self.bytes
622 }
623
624 /// Pure-read walk over the intrusive `all`
625 /// objects list, invoking `visit(tag)` once per live (or
626 /// not-yet-swept) GC-managed object. Used by `luna-tools`'s
627 /// `luna-heap-dump` to build a per-type histogram; embedders
628 /// can reuse it for ad-hoc heap introspection.
629 ///
630 /// # Read-only contract
631 ///
632 /// The callback receives only the [`ObjTag`] discriminant and
633 /// is invoked under a `&self` borrow on the heap: no pointer
634 /// to the GC payload escapes, no `as_mut`-style aliasing is
635 /// available, and the walk performs zero allocation in the
636 /// loop. Safe to call between dispatch ticks (the only allocs
637 /// happen in the caller's bookkeeping).
638 ///
639 /// The walk visits both the live `all` list and the
640 /// `sweep_cur` detached list so a mid-cycle invocation reports
641 /// the same total as [`Heap::live_objects`].
642 pub fn walk_objects(&self, mut visit: impl FnMut(ObjTag)) {
643 for head in [self.all, self.sweep_cur] {
644 let mut cur = head;
645 while !cur.is_null() {
646 // SAFETY: pointers come from the runtime's
647 // intrusive all-objects list. `&self` borrow on
648 // the heap prevents concurrent mutation; the GC
649 // cannot run while this walk holds the borrow,
650 // so every `next` link is valid until consumed.
651 let (tag, next) = unsafe { ((*cur).tag, (*cur).next) };
652 visit(tag);
653 cur = next;
654 }
655 }
656 }
657
658 /// Whether allocation has crossed the auto-GC threshold (cheap safe-point
659 /// check for the interpreter loop).
660 #[inline(always)]
661 pub fn gc_due(&self) -> bool {
662 !self.gc_stopped && self.bytes >= self.next_gc
663 }
664
665 /// `collectgarbage("stop"/"restart")`: suspend or resume auto-GC.
666 pub(crate) fn gc_is_stopped(&self) -> bool {
667 self.gc_stopped
668 }
669
670 pub(crate) fn gc_set_stopped(&mut self, stopped: bool) {
671 self.gc_stopped = stopped;
672 }
673
674 /// Re-arm with caller-supplied `pause` (PUC param, % of live bytes). The
675 /// next cycle fires once `bytes >= live * pause / 100`. `pause=200` (PUC
676 /// default) waits for the heap to double; `pause=100` fires immediately
677 /// when alloc resumes; `pause=300` is 3× — lower pause = more aggressive.
678 pub fn rearm_gc_pause(&mut self, pause: i64) {
679 let pause = pause.max(0) as usize;
680 let target = self
681 .bytes
682 .saturating_mul(pause)
683 .saturating_div(100)
684 .max(GC_MIN_THRESHOLD);
685 self.next_gc = target;
686 }
687
688 /// Re-arm the auto-GC threshold after a collection (PUC pause-style: next
689 /// collection once the live set roughly doubles).
690 pub fn rearm_gc(&mut self) {
691 self.next_gc = self.bytes.saturating_mul(2).max(GC_MIN_THRESHOLD);
692 }
693
694 /// Apply a `before → after` box-size delta from a Table mutation
695 /// (`set`/`rehash`/`ensure_*`). Grows credit `Heap.bytes`; shrinks
696 /// debit it. `free_obj` for `ObjTag::Table` then subtracts the table's
697 /// final `internal_bytes()` so the round-trip is symmetric across the
698 /// table's whole lifetime.
699 pub(crate) fn apply_bytes_delta(&mut self, before: usize, after: usize) {
700 if after > before {
701 self.bytes += after - before;
702 } else if before > after {
703 self.bytes = self.bytes.saturating_sub(before - after);
704 }
705 }
706
707 /// Mark from `roots`, sweep everything unreachable. Returns the number of
708 /// objects freed.
709 pub fn collect(&mut self, roots: &[Value]) -> usize {
710 self.collect_ex(roots, &[])
711 }
712
713 /// Like `collect`, with additional bare-object roots (e.g. the VM's open
714 /// upvalues, which are not first-class Values).
715 pub(crate) fn collect_ex(&mut self, roots: &[Value], extra: &[*mut GcHeader]) -> usize {
716 // a full STW collection subsumes any in-flight incremental cycle:
717 // drive it to completion (Propagate → atomic → Sweep → Pause) so `all`
718 // holds the whole heap again with all marks cleared, then run a fresh
719 // STW cycle. Any tobefnz from the finished cycle stays queued and is
720 // re-marked (kept alive) by the upcoming mark_all so the VM's
721 // run_finalizers can still see them.
722 if self.phase == GcPhase::Propagate {
723 self.gc_finish_atomic();
724 }
725 if self.phase == GcPhase::Sweep {
726 self.gc_sweep_step(usize::MAX);
727 }
728 self.mark_all(roots, extra);
729 self.full_sweep()
730 }
731
732 /// Stop-the-world mark from `roots`/`extra`. Builds an ephemeral marker,
733 /// seeds from roots + extra + tobefnz + any barrier-carried gray queue,
734 /// propagates to completion, then runs the atomic tail (weak / ephemeron
735 /// / finalizer resurrection / current-white flip). After return all
736 /// reachable objects are BLACK and `current_white` has flipped, so the
737 /// caller's sweep tests `other-white` for dead. Does NOT change `phase`.
738 fn mark_all(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
739 let mut m = Marker {
740 stack: Vec::new(),
741 weak: Vec::new(),
742 ephemeron: Vec::new(),
743 no_ephemeron: self.no_ephemeron,
744 cached_protos: Vec::new(),
745 };
746 // Drain any barrier-grayed objects carried over: each was demoted from
747 // BLACK back to gray by a write barrier and is awaiting (re-)trace.
748 m.stack.append(&mut self.gray);
749 for &r in roots {
750 m.value(r);
751 }
752 for &h in extra {
753 m.header(h);
754 }
755 // objects already queued for finalization but not yet run must stay
756 // alive until the VM calls their `__gc` (they may be unreachable now).
757 for &h in &self.tobefnz {
758 m.header(h);
759 }
760 drain_marker(&mut m);
761 // ephemeron convergence: a weak-key entry's value is reachable only if
762 // the key is. Marking a value can make another key reachable, so repeat
763 // until no value is newly marked (PUC convergeephemerons).
764 if !m.ephemeron.is_empty() {
765 loop {
766 let mut changed = false;
767 let eph = m.ephemeron.clone();
768 for t in eph {
769 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
770 changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
771 }
772 drain_marker(&mut m);
773 if !changed {
774 break;
775 }
776 }
777 }
778 self.atomic_tail(&mut m);
779 }
780
781 /// PUC `atomic()` tail: weak-table value-clear, finalizer resurrection,
782 /// post-resurrection ephemeron convergence, proto cache, key-clear, late
783 /// value-clear, and current-white flip. Marker is consumed; `weak` is
784 /// empty on return.
785 ///
786 /// Shared between the STW path (`mark_all`) and the incremental path
787 /// (`gc_finish_atomic`). PUC 5.5 `lgc.c::atomic` mirror:
788 /// propagate → remarkupvals → convergeephemerons
789 /// → clearbyvalues(weak, NULL) ─ early value-clear
790 /// → clearbyvalues(allweak, NULL) ─ (same pass under luna)
791 /// → origweak = g->weak ─ snapshot pre-resurrection
792 /// → separatetobefnz(0) + markbeingfnz ─ separate_finalizables
793 /// → propagateall + convergeephemerons ─ post-resurrection
794 /// → clearbykeys(ephemeron) + clearbykeys(allweak)
795 /// → clearbyvalues(weak, origweak) ─ NEW (post-resurrect) only
796 /// → clearbyvalues(allweak, origall) ─ (same)
797 /// The `origweak` split matters because finalizer resurrection can
798 /// re-trace fresh proto/closure → new weak tables joining `m.weak`;
799 /// PUC limits the late value-clear to those new heads.
800 fn atomic_tail(&mut self, m: &mut Marker) {
801 let early_is_dead = |v: Value| -> bool {
802 let h = match v {
803 Value::Str(_) => return false,
804 Value::Table(t) => t.as_ptr() as *mut GcHeader,
805 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
806 Value::Native(n) => n.as_ptr() as *mut GcHeader,
807 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
808 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
809 _ => return false,
810 };
811 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
812 unsafe { is_white((*h).flags) }
813 };
814 let mark_string = |v: Value| {
815 if let Value::Str(s) = v {
816 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
817 unsafe {
818 let h = s.as_ptr() as *mut GcHeader;
819 // strings are leaves: skip gray and go straight to black
820 (*h).flags = ((*h).flags & !COLOR_BITS) | BLACK;
821 }
822 }
823 };
824 // (1) early clearbyvalues — drop dead-value entries from every weak
825 // table on `m.weak` (PUC's combined `clearbyvalues(weak, NULL) +
826 // clearbyvalues(allweak, NULL)`). Keys are deferred to the
827 // post-resurrection sweep below.
828 for t in &m.weak {
829 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
830 unsafe {
831 let (_wk, wv) = (**t).weak_mode();
832 if wv {
833 (**t).clear_weak(false, true, &early_is_dead, &mark_string);
834 }
835 }
836 }
837 // (2) `origweak` snapshot — PUC takes the list head; luna's `m.weak`
838 // is a Vec, so the equivalent is its length before resurrection.
839 // Anything appended past this index is a "NEW" weak table that the
840 // resurrection pass brought into view.
841 let origweak_n = m.weak.len();
842 // (3) separate + markbeingfnz — resurrect every registered finalizable
843 // that ended up unmarked. `m.header(h)` enqueues each into the marker
844 // so the following drain_marker propagates through it.
845 self.separate_finalizables(m);
846 drain_marker(m);
847 // (4) post-resurrection ephemeron convergence — a resurrected
848 // finalizable may bring new keys into reach, which in turn marks new
849 // ephemeron values.
850 if !m.ephemeron.is_empty() {
851 loop {
852 let mut changed = false;
853 let eph = m.ephemeron.clone();
854 for t in eph {
855 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
856 changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, m) };
857 }
858 drain_marker(m);
859 if !changed {
860 break;
861 }
862 }
863 }
864 // (5) closure-cache weak refs — PUC `traverseproto` clears
865 // `Proto.cache` when the cached LClosure ended the cycle unmarked.
866 // Without this, an LClosure whose only outstanding reference is the
867 // proto's cache would survive forever and its upvalues' `__gc`
868 // finalisers would never run.
869 for &p in &m.cached_protos {
870 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
871 unsafe {
872 if let Some(c) = (*p).cache.get() {
873 let h = c.as_ptr() as *mut GcHeader;
874 if is_white((*h).flags) {
875 (*p).cache.set(None);
876 }
877 }
878 }
879 }
880 // (6) clearbykeys — drop entries whose weak key did not survive
881 // marking, across every weak table (PUC's `clearbykeys(ephemeron)
882 // + clearbykeys(allweak)`). Pure key sweep — value-dead entries are
883 // either already nil from step (1) or wait for step (7).
884 for t in &m.weak {
885 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
886 unsafe {
887 let (wk, _wv) = (**t).weak_mode();
888 if wk {
889 (**t).clear_weak(true, false, &early_is_dead, &mark_string);
890 }
891 }
892 }
893 // (7) late clearbyvalues — PUC's `clearbyvalues(weak, origweak) +
894 // clearbyvalues(allweak, origall)`. Limit the sweep to NEW heads so
895 // we don't redo work already done in step (1) for the pre-resurrect
896 // tables (they were drained by then and re-marking happens through
897 // mark_string in step (6)). resurrected weak tables joining `m.weak`
898 // past `origweak_n` get their first value-clear here.
899 let weak_snapshot = std::mem::take(&mut m.weak);
900 for t in &weak_snapshot[origweak_n..] {
901 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
902 unsafe {
903 let (_wk, wv) = (**t).weak_mode();
904 if wv {
905 (**t).clear_weak(false, true, &early_is_dead, &mark_string);
906 }
907 }
908 }
909 // PUC 5.5 `atomic` end: flip currentwhite so survivors (presently
910 // BLACK) get transitioned into the *new* current-white during sweep,
911 // and the pre-flip current-white becomes the dead-white (the bit the
912 // sweep tests for). Born-during-sweep allocations stamp the new
913 // current-white via `Heap::link`, so they survive this cycle.
914 self.current_white ^= WHITE_BITS;
915 // `gc-verify` — tricolor invariant check at the one
916 // moment it is exact: marking is complete, nothing is freed yet.
917 // A BLACK (surviving) table holding a dead-white child means a
918 // write barrier was missed; the child will be freed by the
919 // upcoming sweep and the table left holding a dangling pointer.
920 // Nothing is dangling *yet*, so the reporter may safely print
921 // string contents to identify the key.
922 #[cfg(feature = "gc-verify")]
923 self.verify_tricolor("atomic_tail");
924 }
925
926 /// `gc-verify` — the set of every live object header
927 /// (all + sweep_cur + finalizer queues), for callers that need to
928 /// audit their own containers (e.g. the VM auditing its register
929 /// stack after a collect).
930 #[cfg(feature = "gc-verify")]
931 pub fn debug_live_set(&self) -> std::collections::HashSet<usize> {
932 let mut live = std::collections::HashSet::new();
933 // SAFETY: heap-owned intrusive lists; all elements are live
934 // allocations.
935 unsafe {
936 let mut cur = self.all;
937 while !cur.is_null() {
938 live.insert(cur as usize);
939 cur = (*cur).next;
940 }
941 let mut cur = self.sweep_cur;
942 while !cur.is_null() {
943 live.insert(cur as usize);
944 cur = (*cur).next;
945 }
946 }
947 for &h in &self.finalize {
948 live.insert(h as usize);
949 }
950 for &h in &self.tobefnz {
951 live.insert(h as usize);
952 }
953 live
954 }
955
956 /// See call site in [`Heap::atomic_tail`]. Panics on the first
957 /// BLACK table whose collectable child is dead-white (missed
958 /// barrier — that child is about to be swept while still
959 /// referenced).
960 #[cfg(feature = "gc-verify")]
961 fn verify_tricolor(&self, ctx: &str) {
962 let new_white = self.current_white;
963 // SAFETY: `all` is the heap's own intrusive list; every element
964 // is a live allocation (nothing has been freed this cycle yet).
965 unsafe {
966 let is_live = |v: Value| -> bool {
967 let h = match v {
968 Value::Str(s) => s.as_ptr() as *mut GcHeader,
969 Value::Table(t) => t.as_ptr() as *mut GcHeader,
970 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
971 Value::Native(n) => n.as_ptr() as *mut GcHeader,
972 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
973 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
974 _ => return true,
975 };
976 let f = (*h).flags;
977 !(is_white(f) && (f & new_white) == 0)
978 };
979 let mut cur = self.all;
980 while !cur.is_null() {
981 let f = (*cur).flags;
982 if (*cur).tag == ObjTag::Table && (f & BLACK) != 0 {
983 let t = &*(cur as *const Table);
984 // Weak tables may legitimately reference dead objects
985 // between clear passes; they were just cleared above,
986 // but their sanctioned dead_key nodes are skipped by
987 // verify_refs anyway. Only strong tables assert.
988 let (wk, wv) = t.weak_mode();
989 if wk || wv {
990 cur = (*cur).next;
991 continue;
992 }
993 let tp = cur as usize;
994 t.verify_refs(&is_live, &|what, idx, v| {
995 // Pre-sweep: dereferencing v is still safe — name
996 // the key when it is a string.
997 let detail = match v.as_bytes() {
998 Some(b) => format!("str {:?}", String::from_utf8_lossy(b)),
999 None => format!(
1000 "non-str {:#x}",
1001 match v {
1002 Value::Table(t) => t.as_ptr() as usize,
1003 Value::Closure(c) => c.as_ptr() as usize,
1004 Value::Coro(c) => c.as_ptr() as usize,
1005 Value::Userdata(u) => u.as_ptr() as usize,
1006 Value::Native(n) => n.as_ptr() as usize,
1007 _ => 0,
1008 }
1009 ),
1010 };
1011 panic!(
1012 "[gc-verify] {ctx}: BLACK table {tp:#x} holds dead-white {what} \
1013 (slot {idx}): {detail} — missed write barrier"
1014 );
1015 });
1016 }
1017 cur = (*cur).next;
1018 }
1019 }
1020 }
1021
1022 /// Borrow Heap's persistent propagate state as an ephemeral Marker.
1023 /// Caller MUST call `stash_marker` with the same Marker after work to
1024 /// write the (potentially mutated) state back. Used by the incremental
1025 /// Propagate path to avoid lifetime entanglement between `&mut self` and
1026 /// `&mut self.propagate`.
1027 fn loan_marker(&mut self) -> Marker {
1028 let mut prop = self
1029 .propagate
1030 .take()
1031 .expect("propagate state taken outside Propagate phase");
1032 Marker {
1033 stack: std::mem::take(&mut self.gray),
1034 weak: std::mem::take(&mut prop.weak),
1035 ephemeron: std::mem::take(&mut prop.ephemeron),
1036 no_ephemeron: prop.no_ephemeron,
1037 cached_protos: std::mem::take(&mut prop.cached_protos),
1038 }
1039 }
1040
1041 fn stash_marker(&mut self, m: Marker) {
1042 let no_ephemeron = m.no_ephemeron;
1043 self.gray = m.stack;
1044 self.propagate = Some(PropagateState {
1045 weak: m.weak,
1046 ephemeron: m.ephemeron,
1047 cached_protos: m.cached_protos,
1048 no_ephemeron,
1049 });
1050 }
1051
1052 /// Begin an incremental mark cycle: seed the persistent gray queue from
1053 /// roots + extra + tobefnz + any barrier-carried gray, install a fresh
1054 /// PropagateState, and enter `GcPhase::Propagate`. Precondition: `Pause`.
1055 pub(crate) fn gc_start_propagate(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
1056 debug_assert!(self.phase == GcPhase::Pause);
1057 self.phase = GcPhase::Propagate;
1058 self.propagate = Some(PropagateState {
1059 weak: Vec::new(),
1060 ephemeron: Vec::new(),
1061 cached_protos: Vec::new(),
1062 no_ephemeron: self.no_ephemeron,
1063 });
1064 let mut m = self.loan_marker();
1065 for &r in roots {
1066 m.value(r);
1067 }
1068 for &h in extra {
1069 m.header(h);
1070 }
1071 for &h in &self.tobefnz {
1072 m.header(h);
1073 }
1074 self.stash_marker(m);
1075 }
1076
1077 /// Drain up to `budget` gray objects (blacken + trace). Returns true if
1078 /// the gray queue is now empty (caller should follow up with
1079 /// `gc_finish_atomic`). PUC `propagatemark` budgeted loop.
1080 pub(crate) fn gc_step_propagate(&mut self, budget: usize) -> bool {
1081 debug_assert!(self.phase == GcPhase::Propagate);
1082 let mut m = self.loan_marker();
1083 let mut n = 0;
1084 while n < budget {
1085 let Some(h) = m.stack.pop() else {
1086 break;
1087 };
1088 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1089 unsafe {
1090 (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1091 match (*h).tag {
1092 ObjTag::Str => {}
1093 ObjTag::Table => (*(h as *mut Table)).trace(&mut m),
1094 ObjTag::Proto => (*(h as *mut Proto)).trace(&mut m),
1095 ObjTag::Closure => (*(h as *mut LuaClosure)).trace(&mut m),
1096 ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(&mut m),
1097 ObjTag::Native => (*(h as *mut NativeClosure)).trace(&mut m),
1098 ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(&mut m),
1099 ObjTag::Userdata => (*(h as *mut Userdata)).trace(&mut m),
1100 }
1101 }
1102 n += 1;
1103 }
1104 let exhausted = m.stack.is_empty();
1105 self.stash_marker(m);
1106 exhausted
1107 }
1108
1109 /// Conclude a Propagate cycle: drain any residual gray, run the atomic
1110 /// tail (weak / ephemeron / finalizer / proto-cache / flip), detach `all`
1111 /// into `sweep_cur`, and enter `GcPhase::Sweep`. Releases `propagate`.
1112 /// PUC `atomic` + `entersweep` transition.
1113 pub(crate) fn gc_finish_atomic(&mut self) {
1114 debug_assert!(self.phase == GcPhase::Propagate);
1115 let mut m = self.loan_marker();
1116 // any residual gray (caller may not have drained to empty)
1117 drain_marker(&mut m);
1118 // pre-atomic ephemeron convergence
1119 if !m.ephemeron.is_empty() {
1120 loop {
1121 let mut changed = false;
1122 let eph = m.ephemeron.clone();
1123 for t in eph {
1124 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1125 changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
1126 }
1127 drain_marker(&mut m);
1128 if !changed {
1129 break;
1130 }
1131 }
1132 }
1133 self.atomic_tail(&mut m);
1134 // PropagateState consumed; transition to Sweep phase by detaching
1135 // the whole heap into sweep_cur (mirrors gc_mark_atomic). Anything
1136 // allocated past this point links onto fresh `all` and survives.
1137 self.propagate = None;
1138 debug_assert!(self.gray.is_empty(), "gray queue not drained at atomic");
1139 self.sweep_cur = std::mem::replace(&mut self.all, ptr::null_mut());
1140 self.phase = GcPhase::Sweep;
1141 }
1142
1143 /// Phase peek (for the VM-side step driver).
1144 pub(crate) fn gc_phase_is_pause(&self) -> bool {
1145 self.phase == GcPhase::Pause
1146 }
1147 pub(crate) fn gc_phase_is_propagate(&self) -> bool {
1148 self.phase == GcPhase::Propagate
1149 }
1150 #[allow(dead_code)] // public phase-peek API trio; sweep variant unused internally
1151 pub(crate) fn gc_phase_is_sweep(&self) -> bool {
1152 self.phase == GcPhase::Sweep
1153 }
1154
1155 /// Forward write barrier: when a BLACK `parent` acquires a fresh reference
1156 /// to a WHITE `child`, gray the child (strings go straight to BLACK as
1157 /// leaves) and push onto the persistent gray queue so the next propagate
1158 /// step traces it. Mirrors PUC `luaC_barrier_`. No-op outside Propagate
1159 /// (parent is gray or white — the mutator never sees a BLACK object live
1160 /// outside an incremental cycle).
1161 #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1162 pub fn barrier_forward(&mut self, parent: *mut GcHeader, child: Value) {
1163 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1164 unsafe {
1165 if !is_black((*parent).flags) {
1166 return;
1167 }
1168 let ch = match child {
1169 Value::Str(s) => s.as_ptr() as *mut GcHeader,
1170 Value::Table(t) => t.as_ptr() as *mut GcHeader,
1171 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1172 Value::Native(n) => n.as_ptr() as *mut GcHeader,
1173 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1174 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1175 _ => return,
1176 };
1177 let cf = (*ch).flags;
1178 if !is_white(cf) {
1179 return;
1180 }
1181 if (*ch).tag == ObjTag::Str {
1182 (*ch).flags = (cf & !COLOR_BITS) | BLACK;
1183 } else {
1184 (*ch).flags = cf & !WHITE_BITS;
1185 self.gray.push(ch);
1186 }
1187 }
1188 }
1189
1190 /// Backward write barrier for objects with many fields (tables, threads):
1191 /// demote the parent itself back to gray so propagate re-traces it.
1192 /// Mirrors PUC `luaC_barrierback_`. One call covers any number of
1193 /// subsequent stores until the next propagate finishes — much cheaper for
1194 /// tables than per-child forward barriers. No-op outside Propagate.
1195 #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1196 pub fn barrier_back(&mut self, parent: *mut GcHeader) {
1197 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1198 unsafe {
1199 let f = (*parent).flags;
1200 if !is_black(f) {
1201 return;
1202 }
1203 (*parent).flags = f & !COLOR_BITS;
1204 self.gray.push(parent);
1205 }
1206 }
1207
1208 /// Move every registered finalizable that is now unreachable to `tobefnz`
1209 /// and resurrect it (mark it via `m`) so it — and the data its `__gc` needs
1210 /// — survives this cycle. Survivors stay registered in `finalize`. PUC's
1211 /// `separatetobefnz` walks `g->finobj` head-first, but `g->finobj` is a
1212 /// linked list that registration *prepends* to — so dead objects end up
1213 /// in `tobefnz` in reverse registration order, and `__gc` ultimately
1214 /// runs LIFO. luna's `finalize` is a Vec that grows forward, so iterate
1215 /// it in reverse here to match the LIFO contract (gc.lua's userdata
1216 /// section asserts the finalizers fire from value 10 back to 0).
1217 fn separate_finalizables(&mut self, m: &mut Marker) {
1218 let mut i = self.finalize.len();
1219 while i > 0 {
1220 i -= 1;
1221 let h = self.finalize[i];
1222 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1223 if unsafe { is_white((*h).flags) } {
1224 // Two-pass cycle-finalize (PUC 5.3 gc.lua :502): when a
1225 // finalizable table holds onto an unreachable coroutine, the
1226 // cycle (table → coroutine.stack → closure → table) keeps the
1227 // mark phase from reaching the table even though it is still
1228 // logically alive for one more GC pass. PUC's mark-sweep wakes
1229 // it via `markbeingfnz` *after* sweeping, so the actual `__gc`
1230 // call lands one cycle later. luna mirrors this by resurrecting
1231 // the table on the first sighting and only enqueuing it for
1232 // `__gc` on the second.
1233 let in_thread_cycle = self.defer_thread_cycle_finalize
1234 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1235 && unsafe { (*h).tag } == ObjTag::Table
1236 && {
1237 let t = h as *mut Table;
1238 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1239 unsafe { (*t).refs_contain_unmarked_coro() }
1240 };
1241 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1242 let already_deferred = unsafe { (*h).flags & DEFERRED != 0 };
1243 if in_thread_cycle && !already_deferred {
1244 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1245 unsafe { (*h).flags |= DEFERRED };
1246 m.header(h);
1247 continue;
1248 }
1249 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1250 unsafe { (*h).flags = ((*h).flags & !(FIN | DEFERRED)) | FINALIZED };
1251 self.tobefnz.push(h);
1252 m.header(h);
1253 self.finalize.swap_remove(i);
1254 } else {
1255 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1256 unsafe { (*h).flags &= !DEFERRED };
1257 }
1258 }
1259 }
1260
1261 /// Register a table for finalization (a live `__gc` metamethod was just set
1262 /// via setmetatable). No-op if it is already pending a finalize (FIN bit).
1263 /// PUC 5.5 reference manual §2.5.3: "An object can be marked again for
1264 /// finalization by calling setmetatable with a different metatable, or
1265 /// with the same metatable but with a different __gc field" — so a
1266 /// previously finalized object (FINALIZED bit set then reset by
1267 /// `take_tobefnz`) can re-register. PUC's `luaC_checkfinalizer` is gated
1268 /// on `tofinalize(o)` only, which mirrors checking the FIN bit.
1269 pub(crate) fn register_finalizable(&mut self, t: Gc<Table>) {
1270 let h = t.as_ptr() as *mut GcHeader;
1271 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1272 unsafe {
1273 if (*h).flags & FIN == 0 {
1274 (*h).flags |= FIN;
1275 self.finalize.push(h);
1276 }
1277 }
1278 }
1279
1280 /// Register a userdata for finalization. PUC 5.1 `newproxy(true)` plus a
1281 /// metatable carrying `__gc` lets a Lua script attach a finalizer to a
1282 /// proxy object — gc.lua's "testing userdata" section binds this
1283 /// behaviour together with weak tables.
1284 pub(crate) fn register_finalizable_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
1285 let h = u.as_ptr() as *mut GcHeader;
1286 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1287 unsafe {
1288 if (*h).flags & FIN == 0 {
1289 (*h).flags |= FIN;
1290 self.finalize.push(h);
1291 }
1292 }
1293 }
1294
1295 /// Every userdata still awaiting finalization, registered or already
1296 /// queued. The io library uses it to reach all open files the way C's
1297 /// `fflush(NULL)` and `exit` reach every `FILE*`.
1298 pub(crate) fn finalizable_userdata(&self) -> Vec<Gc<crate::runtime::Userdata>> {
1299 self.finalize
1300 .iter()
1301 .chain(self.tobefnz.iter())
1302 // SAFETY: both lists hold GcHeader pointers of live objects registered for finalization (heap.rs:5-7); a finalizable object is not freed before its finalizer runs.
1303 .filter(|&&h| unsafe { (*h).tag } == ObjTag::Userdata)
1304 .map(|&h| Gc::from_ptr(h as *mut crate::runtime::Userdata))
1305 .collect()
1306 }
1307
1308 /// Take the objects awaiting their `__gc` call (the VM runs the
1309 /// finalizers). Each entry is dispatched on its `ObjTag` so the caller
1310 /// can look up `__gc` for either a table or a proxy userdata.
1311 ///
1312 /// Mirrors PUC 5.5 `udata2finalize`: the FINALIZED bit is reset on the
1313 /// way out so a `setmetatable(obj, mt_with___gc)` inside (or after) the
1314 /// finalizer can re-register the object for a future round, per the Lua
1315 /// 5.5 reference manual §2.5.3 re-finalize semantics.
1316 pub(crate) fn take_tobefnz(&mut self) -> Vec<crate::runtime::Value> {
1317 use crate::runtime::Value;
1318 std::mem::take(&mut self.tobefnz)
1319 .into_iter()
1320 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1321 .map(|h| unsafe {
1322 (*h).flags &= !FINALIZED;
1323 match (*h).tag {
1324 ObjTag::Table => Value::Table(Gc::from_ptr(h as *mut Table)),
1325 ObjTag::Userdata => {
1326 Value::Userdata(Gc::from_ptr(h as *mut crate::runtime::Userdata))
1327 }
1328 _ => unreachable!("non-finalizable object queued for finalization"),
1329 }
1330 })
1331 .collect()
1332 }
1333
1334 /// Move ALL still-registered finalizables to the pending queue regardless of
1335 /// reachability (PUC separatetobefnz(g, 1) at state close), so the VM can run
1336 /// every `__gc` before the heap is torn down.
1337 pub(crate) fn queue_all_finalizers(&mut self) {
1338 for h in std::mem::take(&mut self.finalize) {
1339 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1340 unsafe { (*h).flags = ((*h).flags & !FIN) | FINALIZED };
1341 self.tobefnz.push(h);
1342 }
1343 }
1344
1345 /// Sweep the whole `all` list in one pass: free dead-white objects,
1346 /// transition survivors (BLACK or current-white) to the new current-white
1347 /// so the next cycle can re-mark them. Returns the number of objects freed.
1348 fn full_sweep(&mut self) -> usize {
1349 // detach the list first so freeing (which needs &mut self for the
1350 // string table) never aliases a pointer into self
1351 let mut freed = 0;
1352 let new_white = self.current_white;
1353 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1354 unsafe {
1355 let mut cur = std::mem::replace(&mut self.all, ptr::null_mut());
1356 let mut kept_head: *mut GcHeader = ptr::null_mut();
1357 let mut kept_tail: *mut GcHeader = ptr::null_mut();
1358 while !cur.is_null() {
1359 let next = (*cur).next;
1360 let f = (*cur).flags;
1361 // dead = other-white (i.e. white but not current-white).
1362 // Survivors are BLACK (just-marked) or current-white (born
1363 // during the sweep itself).
1364 let dead = is_white(f) && (f & new_white) == 0;
1365 if !dead {
1366 (*cur).flags = (f & !COLOR_BITS) | new_white;
1367 (*cur).next = ptr::null_mut();
1368 if kept_tail.is_null() {
1369 kept_head = cur;
1370 } else {
1371 (*kept_tail).next = cur;
1372 }
1373 kept_tail = cur;
1374 } else {
1375 self.free_obj(cur);
1376 freed += 1;
1377 }
1378 cur = next;
1379 }
1380 self.all = kept_head;
1381 }
1382 self.live -= freed;
1383 #[cfg(feature = "gc-verify")]
1384 self.verify_no_dangling("full_sweep");
1385 freed
1386 }
1387
1388 /// Sweep up to `budget` objects from the detached `sweep_cur` list: free
1389 /// unmarked ones, splice marked survivors back onto `all` (clearing their
1390 /// MARK bit). Returns true once the list is exhausted (cycle complete →
1391 /// back to `Pause`). Safe with no write barrier: marking was atomic, so any
1392 /// object still unmarked here was unreachable at mark time and the mutator
1393 /// holds no reference that could have resurrected it.
1394 pub(crate) fn gc_sweep_step(&mut self, budget: usize) -> bool {
1395 let mut n = 0;
1396 let new_white = self.current_white;
1397 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1398 unsafe {
1399 while n < budget && !self.sweep_cur.is_null() {
1400 let cur = self.sweep_cur;
1401 let next = (*cur).next;
1402 let f = (*cur).flags;
1403 let dead = is_white(f) && (f & new_white) == 0;
1404 if !dead {
1405 (*cur).flags = (f & !COLOR_BITS) | new_white;
1406 (*cur).next = self.all;
1407 self.all = cur;
1408 } else {
1409 self.free_obj(cur);
1410 self.live -= 1;
1411 }
1412 self.sweep_cur = next;
1413 n += 1;
1414 }
1415 }
1416 // Verify after EVERY step, not just cycle completion: the
1417 // mutator runs between steps, so a reference dangling mid-cycle
1418 // is already a live bug.
1419 #[cfg(feature = "gc-verify")]
1420 self.verify_no_dangling("sweep_step");
1421 if self.sweep_cur.is_null() {
1422 self.phase = GcPhase::Pause;
1423 true
1424 } else {
1425 false
1426 }
1427 }
1428
1429 /// `gc-verify` — post-sweep dangling-reference check
1430 /// (PUC `lua_checkmemory` analogue). Builds the live set from the
1431 /// `all` list + finalizer queues, then walks every live table's
1432 /// collectable refs via [`Table::verify_refs`]. Panics with table
1433 /// pointer + slot detail on the first reference whose target is no
1434 /// longer live — i.e. at the collect that CREATED the dangling
1435 /// pointer, not at the later allocator-dependent dereference.
1436 #[cfg(feature = "gc-verify")]
1437 pub fn verify_no_dangling(&self, ctx: &str) {
1438 use std::collections::HashSet;
1439 fn value_header(v: Value) -> Option<*mut GcHeader> {
1440 match v {
1441 Value::Str(s) => Some(s.as_ptr() as *mut GcHeader),
1442 Value::Table(t) => Some(t.as_ptr() as *mut GcHeader),
1443 Value::Closure(c) => Some(c.as_ptr() as *mut GcHeader),
1444 Value::Native(n) => Some(n.as_ptr() as *mut GcHeader),
1445 Value::Coro(c) => Some(c.as_ptr() as *mut GcHeader),
1446 Value::Userdata(u) => Some(u.as_ptr() as *mut GcHeader),
1447 _ => None,
1448 }
1449 }
1450 let mut live: HashSet<usize> = HashSet::new();
1451 // SAFETY: `all` / `sweep_cur` / finalizer-queue pointers are the
1452 // heap's own intrusive lists; every element is a live allocation
1453 // until free_obj unlinks it.
1454 unsafe {
1455 let mut cur = self.all;
1456 while !cur.is_null() {
1457 live.insert(cur as usize);
1458 cur = (*cur).next;
1459 }
1460 let mut cur = self.sweep_cur;
1461 while !cur.is_null() {
1462 live.insert(cur as usize);
1463 cur = (*cur).next;
1464 }
1465 for &h in &self.finalize {
1466 live.insert(h as usize);
1467 }
1468 for &h in &self.tobefnz {
1469 live.insert(h as usize);
1470 }
1471 let is_live = |v: Value| -> bool {
1472 match value_header(v) {
1473 None => true,
1474 Some(h) => live.contains(&(h as usize)),
1475 }
1476 };
1477 // Walk BOTH lists: `all` (already-swept survivors) and
1478 // `sweep_cur` (not-yet-swept remainder of an incremental
1479 // cycle). A dangling reference can live in either while the
1480 // mutator runs between sweep steps.
1481 let new_white = self.current_white;
1482 for head in [self.all, self.sweep_cur] {
1483 let mut cur = head;
1484 while !cur.is_null() {
1485 // Skip dead-but-not-yet-swept objects on `sweep_cur`:
1486 // they are unreachable, so a dangling ref inside them
1487 // is benign (their peers may already be freed).
1488 let f = (*cur).flags;
1489 if is_white(f) && (f & new_white) == 0 {
1490 cur = (*cur).next;
1491 continue;
1492 }
1493 if (*cur).tag == ObjTag::Table {
1494 let t = &*(cur as *const Table);
1495 let tp = cur as usize;
1496 t.verify_refs(&is_live, &|what, idx, v| {
1497 // Do NOT Debug-format `v` — for Value::Str that
1498 // would dereference the (dangling) payload.
1499 let p = value_header(v).map(|h| h as usize).unwrap_or(0);
1500 panic!(
1501 "[gc-verify] {ctx}: table {tp:#x} holds dangling {what} \
1502 (slot {idx}, target {p:#x})"
1503 );
1504 });
1505 }
1506 cur = (*cur).next;
1507 }
1508 }
1509 }
1510 }
1511
1512 unsafe fn free_obj(&mut self, h: *mut GcHeader) {
1513 #[cfg(feature = "gc-verify")]
1514 {
1515 self.recently_freed.insert(h as usize);
1516 crate::runtime::gc_verify_probe::FREED.with(|f| f.borrow_mut().insert(h as usize));
1517 }
1518 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1519 unsafe {
1520 match (*h).tag {
1521 ObjTag::Table => {
1522 let t = h as *mut Table;
1523 let internal = (*t).internal_bytes();
1524 self.bytes = self
1525 .bytes
1526 .saturating_sub(std::mem::size_of::<Table>() + internal);
1527 // pool recycle. Drop the
1528 // Box-owned interior (slab, nodes, metatable) so the
1529 // Table struct itself can be re-handed-out by a
1530 // future `new_table` without re-mallocing. Cap pool
1531 // at 4096 entries to bound idle memory.
1532 const TABLE_POOL_CAP: usize = 4096;
1533 if self.table_pool.len() < TABLE_POOL_CAP {
1534 // Free interior heap allocations now (slab + nodes).
1535 // Each Box::new([]) is a dangling no-alloc empty
1536 // slice, so reassigning is just a pointer move.
1537 (*t).slab = Box::new([]);
1538 (*t).nodes = Box::new([]);
1539 // drop the SoA Robin Hood parallel arrays too
1540 // (usually Box::new([]) dangling stubs).
1541 (*t).keys = Box::new([]);
1542 (*t).vals = Box::new([]);
1543 (*t).meta = Box::new([]);
1544 (*t).tombstones = 0;
1545 (*t).iter_depth = 0;
1546 (*t).metatable = None;
1547 // Stash the raw pointer for future reuse.
1548 // SAFETY: t is non-null (came from a live Gc<Table>);
1549 // pool owns it until reuse or Heap::Drop.
1550 self.table_pool.push(std::ptr::NonNull::new_unchecked(t));
1551 } else {
1552 drop(Box::from_raw(t));
1553 }
1554 }
1555 ObjTag::Proto => {
1556 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Proto>());
1557 drop(Box::from_raw(h as *mut Proto));
1558 }
1559 ObjTag::Closure => {
1560 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<LuaClosure>());
1561 drop(Box::from_raw(h as *mut LuaClosure));
1562 }
1563 ObjTag::Upvalue => {
1564 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Upvalue>());
1565 drop(Box::from_raw(h as *mut Upvalue));
1566 }
1567 ObjTag::Native => {
1568 self.bytes = self
1569 .bytes
1570 .saturating_sub(std::mem::size_of::<NativeClosure>());
1571 drop(Box::from_raw(h as *mut NativeClosure));
1572 }
1573 ObjTag::Coro => {
1574 self.bytes = self
1575 .bytes
1576 .saturating_sub(std::mem::size_of::<crate::runtime::Coro>());
1577 drop(Box::from_raw(h as *mut crate::runtime::Coro));
1578 }
1579 ObjTag::Userdata => {
1580 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Userdata>());
1581 drop(Box::from_raw(h as *mut Userdata));
1582 }
1583 ObjTag::Str => {
1584 let s = h as *mut LuaStr;
1585 self.bytes = self.bytes.saturating_sub(string::alloc_size((*s).len()));
1586 if (*s).is_short() {
1587 self.strings.remove(s);
1588 }
1589 string::free(s);
1590 }
1591 }
1592 }
1593 }
1594}
1595
1596impl Drop for Heap {
1597 fn drop(&mut self) {
1598 // free everything regardless of reachability, including any list still
1599 // detached for an in-flight incremental sweep
1600 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1601 unsafe {
1602 for mut cur in [self.all, self.sweep_cur] {
1603 while !cur.is_null() {
1604 let next = (*cur).next;
1605 self.free_obj(cur);
1606 cur = next;
1607 }
1608 }
1609 // release the table_pool's
1610 // dangling Box<Table> ptrs. Each was Box::into_raw'd into
1611 // the pool (via free_obj recycle path); without this, the
1612 // Tables would leak. The pool's Tables had their interior
1613 // Box-owned fields (slab/nodes/metatable) already cleared
1614 // when they were recycled, so dropping the Table now only
1615 // releases the Table struct itself.
1616 for ptr in self.table_pool.drain(..) {
1617 drop(Box::from_raw(ptr.as_ptr()));
1618 }
1619 }
1620 }
1621}
1622
1623impl Default for Heap {
1624 fn default() -> Heap {
1625 Heap::new()
1626 }
1627}
1628
1629/// Mark accumulator: gray stack plus entry points for Values and bare
1630/// object headers (Protos/Upvalues are not first-class Values).
1631pub(crate) struct Marker {
1632 stack: Vec<*mut GcHeader>,
1633 /// live tables with a weak `__mode`, collected during marking and processed
1634 /// (dead weak entries cleared) before the sweep
1635 pub(crate) weak: Vec<*mut Table>,
1636 /// ephemeron tables (weak keys, strong values): their hash values are not
1637 /// marked during trace but in a fixpoint pass keyed on key-reachability
1638 pub(crate) ephemeron: Vec<*mut Table>,
1639 /// PUC 5.1 mode: skip ephemeron handling — `__mode='k'` tables mark their
1640 /// values strongly during the normal trace pass (see [`Heap::no_ephemeron`]).
1641 pub(crate) no_ephemeron: bool,
1642 /// Protos with a non-null closure cache (PUC `Proto.cache`). After
1643 /// marking is done, any cached LClosure that ended the cycle unmarked is
1644 /// cleared so the sweep can collect it — the cache is a *weak* reference
1645 /// (PUC `traverseproto` checks `iswhite(cache)`). Seen via [`Proto::trace`].
1646 pub(crate) cached_protos: Vec<*mut crate::runtime::Proto>,
1647}
1648
1649/// Drain the gray stack: pop each marked object and trace its children until
1650/// the worklist is empty (iterative, so deep graphs don't overflow the Rust
1651/// stack). Shared by the root mark and the post-resurrection remark.
1652fn drain_marker(m: &mut Marker) {
1653 while let Some(h) = m.stack.pop() {
1654 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1655 unsafe {
1656 // PUC `propagatemark`: gray → black before scanning children, so a
1657 // child that points back at us (cycle) re-traces us as already
1658 // black and does not loop. White bits were cleared on push.
1659 (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1660 match (*h).tag {
1661 ObjTag::Str => {}
1662 ObjTag::Table => (*(h as *mut Table)).trace(m),
1663 ObjTag::Proto => (*(h as *mut Proto)).trace(m),
1664 ObjTag::Closure => (*(h as *mut LuaClosure)).trace(m),
1665 ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(m),
1666 ObjTag::Native => (*(h as *mut NativeClosure)).trace(m),
1667 ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(m),
1668 ObjTag::Userdata => (*(h as *mut Userdata)).trace(m),
1669 }
1670 }
1671 }
1672}
1673
1674impl Marker {
1675 /// Mark a value, returning true if it was newly marked (was white).
1676 pub(crate) fn value(&mut self, v: Value) -> bool {
1677 let h = match v {
1678 Value::Str(s) => s.as_ptr() as *mut GcHeader,
1679 Value::Table(t) => t.as_ptr() as *mut GcHeader,
1680 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1681 Value::Native(n) => n.as_ptr() as *mut GcHeader,
1682 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1683 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1684 _ => return false,
1685 };
1686 self.header(h)
1687 }
1688
1689 /// Mark a bare header, returning true if it was newly marked (was white).
1690 /// Transitions white → gray (in PUC `reallymarkobject` terms): clears the
1691 /// current-white bit and pushes onto the gray stack. `drain_marker` later
1692 /// pops it, traces children, and stamps it BLACK.
1693 pub(crate) fn header(&mut self, h: *mut GcHeader) -> bool {
1694 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1695 unsafe {
1696 let f = (*h).flags;
1697 if is_white(f) {
1698 (*h).flags = f & !WHITE_BITS;
1699 self.stack.push(h);
1700 true
1701 } else {
1702 false
1703 }
1704 }
1705 }
1706}
1707
1708/// Whether a value is "alive" for ephemeron key purposes: non-collectable
1709/// values and strings are always alive (strings are never weakly cleared);
1710/// a collectable object is alive only once marked (gray or black).
1711fn weak_key_alive(v: Value) -> bool {
1712 let h = match v {
1713 Value::Table(t) => t.as_ptr() as *mut GcHeader,
1714 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1715 Value::Native(n) => n.as_ptr() as *mut GcHeader,
1716 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1717 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1718 _ => return true, // strings, numbers, booleans: never weak-collected
1719 };
1720 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1721 unsafe { !is_white((*h).flags) }
1722}
1723
1724/// Hash seed from address entropy (ASLR) and clock, luaL_makeseed style.
1725fn make_seed() -> u32 {
1726 let stack_var = 0u8;
1727 let mut h = &stack_var as *const u8 as u64;
1728 h ^= (make_seed as *const () as u64) << 16;
1729 if let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
1730 h ^= (d.subsec_nanos() as u64) << 32 ^ d.as_secs();
1731 }
1732 h ^= h >> 33;
1733 h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
1734 h ^= h >> 33;
1735 h as u32
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740 use super::*;
1741 use crate::vm::isa::{Inst, Op};
1742
1743 #[test]
1744 fn collect_traces_function_objects() {
1745 let mut heap = Heap::new();
1746 let source = heap.intern(b"@test");
1747 let kstr = heap.intern(b"a-constant-string");
1748 let inner = Proto {
1749 hdr: GcHeader::new(ObjTag::Proto),
1750 code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1751 consts: Box::new([]),
1752 protos: Box::new([]),
1753 upvals: Box::new([]),
1754 num_params: 0,
1755 is_vararg: false,
1756 has_vararg_table_pseudo: false,
1757 has_compat_vararg_arg: false,
1758 max_stack: 2,
1759 lines: Box::new([1]),
1760 source,
1761 line_defined: 1,
1762 last_line_defined: 1,
1763 locvars: Box::new([]),
1764 cache: std::cell::Cell::new(None),
1765 jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1766 env_upval_idx: u8::MAX,
1767 trace_hot_count: std::cell::Cell::new(0),
1768 call_hot_count: std::cell::Cell::new(0),
1769 trace_discard_count: std::cell::Cell::new(0),
1770 trace_gave_up: std::cell::Cell::new(false),
1771 trace_compile_failures: crate::jit::send_compat::TRefLock::new(Vec::new()),
1772 traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1773 };
1774 let inner = heap.adopt_proto(inner);
1775 let outer = Proto {
1776 hdr: GcHeader::new(ObjTag::Proto),
1777 code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1778 consts: Box::new([Value::Str(kstr)]),
1779 protos: Box::new([inner]),
1780 upvals: Box::new([]),
1781 num_params: 0,
1782 is_vararg: true,
1783 has_vararg_table_pseudo: false,
1784 has_compat_vararg_arg: false,
1785 max_stack: 2,
1786 lines: Box::new([1]),
1787 source,
1788 line_defined: 0,
1789 last_line_defined: 0,
1790 locvars: Box::new([]),
1791 cache: std::cell::Cell::new(None),
1792 jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1793 env_upval_idx: u8::MAX,
1794 trace_hot_count: std::cell::Cell::new(0),
1795 call_hot_count: std::cell::Cell::new(0),
1796 trace_discard_count: std::cell::Cell::new(0),
1797 trace_gave_up: std::cell::Cell::new(false),
1798 trace_compile_failures: crate::jit::send_compat::TRefLock::new(Vec::new()),
1799 traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1800 };
1801 let outer = heap.adopt_proto(outer);
1802 let captured = heap.intern(b"captured-value-string-xxxxxxxxxxxxxxxxxxxxxxxxx");
1803 let uv = heap.new_upvalue(UpvalState::Closed(Value::Str(captured)));
1804 let cl = heap.new_closure(outer, Box::new([uv]));
1805 // objects: source, kstr, inner, outer, captured, uv, cl
1806 assert_eq!(heap.live_objects(), 7);
1807 // rooting the closure keeps the whole graph alive
1808 assert_eq!(heap.collect(&[Value::Closure(cl)]), 0);
1809 assert_eq!(heap.live_objects(), 7);
1810 assert_eq!(heap.collect(&[]), 7);
1811 assert_eq!(heap.live_objects(), 0);
1812 }
1813
1814 #[test]
1815 fn collect_unreachable() {
1816 let mut heap = Heap::new();
1817 let s = heap.intern(b"hello");
1818 let t = heap.new_table();
1819 assert_eq!(heap.live_objects(), 2);
1820 // both rooted: nothing freed
1821 assert_eq!(heap.collect(&[Value::Str(s), Value::Table(t)]), 0);
1822 // only table rooted: string freed
1823 assert_eq!(heap.collect(&[Value::Table(t)]), 1);
1824 assert_eq!(heap.live_objects(), 1);
1825 // nothing rooted
1826 assert_eq!(heap.collect(&[]), 1);
1827 assert_eq!(heap.live_objects(), 0);
1828 }
1829
1830 #[test]
1831 fn collect_traces_table_contents() {
1832 let mut heap = Heap::new();
1833 let t = heap.new_table();
1834 let k = heap.intern(b"key-string-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // long
1835 let v = heap.intern(b"val");
1836 unsafe { t.as_mut() }
1837 .set(&mut heap, Value::Str(k), Value::Str(v))
1838 .unwrap();
1839 let inner = heap.new_table();
1840 unsafe { t.as_mut() }
1841 .set(&mut heap, Value::Int(1), Value::Table(inner))
1842 .unwrap();
1843 unsafe { inner.as_mut() }.set_metatable(Some(t));
1844 assert_eq!(heap.live_objects(), 4);
1845 // root only the outer table: everything reachable through it survives
1846 assert_eq!(heap.collect(&[Value::Table(t)]), 0);
1847 assert_eq!(heap.live_objects(), 4);
1848 assert_eq!(heap.collect(&[]), 4);
1849 }
1850
1851 #[test]
1852 fn interned_string_reclaimed_and_reinternable() {
1853 let mut heap = Heap::new();
1854 heap.intern(b"transient");
1855 assert_eq!(heap.collect(&[]), 1);
1856 let s2 = heap.intern(b"transient");
1857 assert_eq!(s2.as_bytes(), b"transient");
1858 assert_eq!(heap.live_objects(), 1);
1859 }
1860
1861 #[test]
1862 fn bytes_and_live_round_trip_to_zero() {
1863 // Memory-invariant audit: after a churn of table allocation, rehash-
1864 // driven growth, and full collection of an empty root set, both
1865 // `heap.bytes` and `heap.live_objects` must return to 0. Catches any
1866 // alloc / free asymmetry in the Table internal-Box delta tracking
1867 // or the live counter (link/sweep symmetry).
1868 let mut heap = Heap::new();
1869 assert_eq!(heap.bytes(), 0);
1870 assert_eq!(heap.live_objects(), 0);
1871 // Build a churn: 50 tables, each filled with 200 int keys (forces
1872 // multiple rehashes); plus interned strings spliced through the
1873 // hash part. Bytes should grow well past the empty baseline.
1874 let mut roots: Vec<Value> = Vec::new();
1875 for ti in 0..50 {
1876 let t = heap.new_table();
1877 for k in 1..=200 {
1878 let _ =
1879 unsafe { t.as_mut() }.set(&mut heap, Value::Int(k), Value::Int(ti * 1000 + k));
1880 }
1881 for sk in 0..32 {
1882 let key = Value::Str(heap.intern(format!("k{ti}-{sk}").as_bytes()));
1883 let _ = unsafe { t.as_mut() }.set(&mut heap, key, Value::Int(sk));
1884 }
1885 roots.push(Value::Table(t));
1886 }
1887 let live_peak = heap.live_objects();
1888 let bytes_peak = heap.bytes();
1889 assert!(live_peak > 0, "live should be >0 after churn");
1890 assert!(bytes_peak > 0, "bytes should be >0 after churn");
1891 // Root only half — the other half should be collected.
1892 let half = roots.len() / 2;
1893 let freed = heap.collect(&roots[..half]);
1894 assert!(freed > 0, "some objects should have been freed");
1895 assert!(
1896 heap.bytes() < bytes_peak,
1897 "bytes must drop after partial collect"
1898 );
1899 assert!(
1900 heap.live_objects() < live_peak,
1901 "live must drop after partial collect"
1902 );
1903 // Drop everything: counters must return to 0 exactly.
1904 drop(roots);
1905 let _ = heap.collect(&[]);
1906 assert_eq!(heap.live_objects(), 0, "live not zero after full collect");
1907 assert_eq!(
1908 heap.bytes(),
1909 0,
1910 "bytes not zero after full collect — asymmetric alloc/free"
1911 );
1912 }
1913
1914 /// Regression test for a string-table use-after-free:
1915 /// `StringTable::intern` must NOT return a dead-white (about-to-be-swept)
1916 /// short-string pointer. Mirrors PUC `luaS_new`'s resurrect-on-hit guard
1917 /// (lstring.c — `if (isdead(g, ts)) changewhite(ts);`).
1918 ///
1919 /// Without the guard, the bucket-chain still references the unswept
1920 /// short string after the atomic flip; a re-`intern` of the same bytes
1921 /// hands back that pointer; the budget-paced sweep then frees it and
1922 /// the next bucket walk dereferences libc-recycled garbage (the
1923 /// `0x800002a80000002d` misaligned pointer, for example).
1924 #[test]
1925 fn intern_resurrects_dead_white_short_string() {
1926 let mut heap = Heap::new();
1927 let alive = heap.intern(b"keep-me-alive-1");
1928 let dying = heap.intern(b"transient-x");
1929 let dying_ptr = dying.as_ptr();
1930 let dying_bytes = dying.as_bytes().to_vec();
1931 // Drive an incremental cycle by hand to reproduce the race:
1932 // 1. mark-propagate with `alive` only as a root → `dying` stays white
1933 // 2. atomic flip → `dying` becomes dead-white, bucket still points at it
1934 // 3. RE-INTERN the same bytes BEFORE sweep clears the bucket
1935 // 4. fix must either (a) skip the dead entry & alloc fresh, or
1936 // (b) resurrect dying back to current-white
1937 let alive_root = [Value::Str(alive)];
1938 heap.gc_start_propagate(&alive_root, &[]);
1939 while !heap.gc_step_propagate(usize::MAX) {}
1940 heap.gc_finish_atomic();
1941 // At this point sweep_cur holds the detached old-heap list and the
1942 // dying string is dead-white. The bucket chain in `self.strings`
1943 // still references it.
1944 let resurrected = heap.intern(&dying_bytes);
1945 // Two valid outcomes:
1946 // * resurrect: same pointer, but flagged current-white so sweep
1947 // will keep it alive (PUC luaS_new shape)
1948 // * skip-and-alloc-fresh: different pointer, dying gets swept
1949 // normally as it should
1950 // Either way, after completing the sweep the heap must NOT crash
1951 // when we try to intern more short strings (which walks bucket chains).
1952 while !heap.gc_sweep_step(usize::MAX) {}
1953 // Smoke: bucket-chain walk for a fresh string must not deref a
1954 // freed pointer.
1955 let _fresh = heap.intern(b"after-sweep-canary");
1956 // If the fix is "resurrect", same pointer + bytes preserved:
1957 if resurrected.as_ptr() == dying_ptr {
1958 assert_eq!(resurrected.as_bytes(), dying_bytes.as_slice());
1959 }
1960 // Final cleanup: full collect must complete without UAF.
1961 drop(heap);
1962 }
1963
1964 #[test]
1965 fn deep_table_chain_marks_iteratively() {
1966 // deep chain: explicit mark stack must not overflow (smaller under
1967 // miri — the interpreter makes 100k tables take ~30 minutes)
1968 let n = if cfg!(miri) { 2_000 } else { 100_000 };
1969 let mut heap = Heap::new();
1970 let head = heap.new_table();
1971 let mut cur = head;
1972 for _ in 0..n {
1973 let next = heap.new_table();
1974 unsafe { cur.as_mut() }
1975 .set(&mut heap, Value::Int(1), Value::Table(next))
1976 .unwrap();
1977 cur = next;
1978 }
1979 assert_eq!(heap.collect(&[Value::Table(head)]), 0);
1980 assert_eq!(heap.collect(&[]), n + 1);
1981 }
1982}