lua-vm 0.0.33

A Lua 5.4 interpreter implemented in safe Rust.
Documentation
//! Phase-D `Trace` implementations for GC-rooted types defined in this
//! crate. Types in `lua-types` (LuaValue, LuaString, UpVal) have their
//! Trace impls in `lua-types/src/trace_impls.rs` because of Rust's orphan
//! rule.
//!
//! Each impl below is a `todo!("phase-d: trace X")` stub. The
//! panic-driven mega-loop surfaces each one when a runtime path triggers
//! `Heap::full_collect`. Each agent works on ONE type — no family
//! expansion (Trace impls have subtle invariants).
//!
//! Implementation guidance for agents:
//!   1. Read the type definition; enumerate every field
//!   2. For every `Gc<T>`, `GcRef<T>`, or container (Vec/Option/HashMap)
//!      thereof, call `m.mark(field)` or `field.trace(m)` appropriately
//!   3. Skip non-GC fields (primitives, `String`, `Vec<u8>`)
//!   4. Skip "intentionally not traced" fields (weak refs)
//!   5. Reference `reference/lua-5.4.7/src/lgc.c`'s `reallymarkobject`

use crate::state::{FinalizerObject, GlobalState, LuaState};
use crate::string::{LuaStringImpl, LuaUserDataImpl};
use lua_gc::{Marker, Trace};

/// Phase-B internal richer LuaString. The byte buffer is a Rust `Rc<[u8]>`
/// (not GC-managed); no fields to mark.
impl Trace for LuaStringImpl {

    fn type_name(&self) -> &'static str {
        std::any::type_name::<Self>()
    }

    fn trace(&self, _m: &mut Marker) {}
}

/// Phase-B internal userdata. Both `metatable` and `uv` are currently
/// `Option<()>` / `Vec<()>` stubs — no GC edges to walk yet. Becomes
/// real when userdata machinery lands post-D-1.
impl Trace for LuaUserDataImpl {

    fn type_name(&self) -> &'static str {
        std::any::type_name::<Self>()
    }

    fn trace(&self, _m: &mut Marker) {}
}

impl Trace for FinalizerObject {

    fn type_name(&self) -> &'static str {
        std::any::type_name::<Self>()
    }

    fn trace(&self, m: &mut Marker) {
        match self {
            FinalizerObject::Table(t) => t.trace(m),
            FinalizerObject::UserData(u) => u.trace(m),
        }
    }
}

impl Trace for LuaState {

    fn type_name(&self) -> &'static str {
        std::any::type_name::<Self>()
    }

    fn trace(&self, m: &mut Marker) {
        // C's traversethread (lgc.c) walks [stack .. top) and relies on two
        // companion invariants this port mirrors via `gc_trace_bound` (the
        // savestate half — widen to ci.top only for a Lua current frame)
        // and `clear_dead_stack_tail` (the atomic-clear half, run before
        // every collect). Every slot below the bound is therefore
        // valid-or-nil; the old frame-bounded range walk and the saved_pc
        // debug-local heuristic (#140 bug B's two faces) are gone.
        let bound = self.gc_trace_bound();
        for slot in &self.stack[..bound] {
            slot.val.trace(m);
        }

        for uv in self.openupval.iter() {
            uv.trace(m);
        }

        // PORT NOTE: `global` (Rc<RefCell<GlobalState>>) is reached from the
        // heap's root via GlobalState::trace; tracing it from each thread
        // would re-enter the root and is explicitly excluded.
        // PORT NOTE: `call_info` entries carry pc offsets and stack indices
        // but no direct GcRef fields. The active closure is reached through
        // the stack slot at `ci.func`, already covered by the stack walk.
        // PORT NOTE: `tbclist` holds StackIdx values only; the to-be-closed
        // objects themselves live on the stack and are traced there.
    }
}

impl Trace for GlobalState {

    fn type_name(&self) -> &'static str {
        std::any::type_name::<Self>()
    }

    fn trace(&self, m: &mut Marker) {
        // per-type metatables, and pending finalizers. We expand the set to
        // include preallocated short strings (memerrmsg, tmname[]) and the
        // open-upvalue thread list, both of which the panic-driven Phase-D
        // mega-loop expects to see at the root.

        self.l_registry.trace(m);

        // Values held by Rust-side embedding handles are rooted outside the
        // Lua registry table so handle Drop can unroot without touching the
        // Lua stack/API. They are still ordinary GC roots during marking.
        for value in self.external_roots.iter_values() {
            value.trace(m);
        }

        // Cross-thread open-upvalue mirrors are live roots while a coroutine
        // resume holds the home thread's stack behind an outer mutable borrow.
        for value in self.cross_thread_upvals.values() {
            value.trace(m);
        }

        // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
        // storage-less, so `globals` and `loaded` cannot live inside the registry
        // table (see `init_registry`). They are kept as direct GlobalState fields
        // and must be traced explicitly as roots; once the placeholder reconciles
        // with vm::LuaTable, these become reachable via `l_registry` and the two
        // lines below disappear.
        self.globals.trace(m);
        self.loaded.trace(m);

        if let Some(t) = &self.mainthread {
            t.trace(m);
        }

        self.main_thread_value.trace(m);

        if self.current_thread_id != self.main_thread_id {
            if let Some(entry) = self.threads.get(&self.current_thread_id) {
                entry.value.trace(m);
            }
        }

        // Registered coroutines are not roots by registration alone. The
        // post-mark hook traces stacks only for thread handles that were
        // reached from a real root, matching Lua's collectable coroutine
        // semantics.

        for slot in self.mt.iter() {
            if let Some(t) = slot {
                t.trace(m);
            }
        }

        for s in self.tmname.iter() {
            s.trace(m);
        }

        self.memerrmsg.trace(m);

        for th in self.twups.iter() {
            th.trace(m);
        }

        // `interned_lt` is a weak short-string cache. The collector prunes
        // unmarked entries from the post-mark hook instead of tracing them as
        // roots here.
        for row in self.strcache.iter() {
            for s in row.iter() {
                s.trace(m);
            }
        }

        // Pending finalizers are NOT traced here — that's what lets the mark
        // phase distinguish "still reachable from the user program" from
        // "only kept alive by the finalizer registry". `collect_via_heap`'s
        // post-mark hook checks each entry against the visited set; an
        // unvisited entry is moved to `to_be_finalized` and explicitly
        // marked there so it survives the sweep.
        //
        // `to_be_finalized` IS traced as a strong root: objects in this list
        // are awaiting their `__gc` call but are otherwise dead, and the
        // object (plus its descendants) must survive long enough for the
        // finalizer to run.
        for object in self.finalizers.to_be_finalized().iter() {
            object.trace(m);
        }

        // Trace suspended parent stacks. When a coroutine is running, any
        // parent threads are suspended and their stacks are not reachable from
        // `threads` (which only holds coroutines, not the main thread). Before
        // `aux_resume` resumes a coroutine it pushes a snapshot of the parent's
        // live stack onto `suspended_parent_stacks` so those GC-managed values
        // remain marked during collections triggered from inside the coroutine.
        for stack_snapshot in self.suspended_parent_stacks.iter() {
            for v in stack_snapshot.iter() {
                v.trace(m);
            }
        }
        for upval_snapshot in self.suspended_parent_open_upvals.iter() {
            for uv in upval_snapshot.iter() {
                uv.trace(m);
            }
        }

        // PORT NOTE: `strt` (the internal LuaStringImpl intern table) is a
        // weak table in C; entries are cleared during the atomic weak-table
        // pass (`clearbykeys`), not marked as roots. The current port has no
        // incremental weak-sweep, but `strt` is keyed by byte-content rather
        // than by `Gc` identity, so a dangling entry there is silently
        // recreated by the next `intern_str` — no UAF, unlike `interned_lt`.
        // PORT NOTE: `fixedgc` holds objects pre-marked fixed/black at
        // allocation (`luaC_fix`); the mark phase never re-visits them, and
        // `dyn Collectable` does not implement `Trace` here.
        // PORT NOTE: `allgc`, `finobj`, `gray`, `grayagain`, `tobefnz`,
        // `weak`, `ephemeron`, `allweak` are GC bookkeeping lists owned by
        // `heap` — they are the universe of allocated objects, not roots.
    }
}

// ──────────────────────────────────────────────────────────────────────────────
// PORT STATUS
//   source:        n/a (GC Trace impls bridging lua-vm and lua-gc)
//   target_crate:  lua-vm
//   confidence:    high
//   todos:         0
//   port_notes:    0
//   unsafe_blocks: 0
//   notes:         Implements lua_gc::Trace for LuaState + GlobalState. C does this via
//                  hand-written mark routines in lgc.c; we use a trait dispatch.
// ──────────────────────────────────────────────────────────────────────────────