Skip to main content

lua_vm/
trace_impls.rs

1//! Phase-D `Trace` implementations for GC-rooted types defined in this
2//! crate. Types in `lua-types` (LuaValue, LuaString, UpVal) have their
3//! Trace impls in `lua-types/src/trace_impls.rs` because of Rust's orphan
4//! rule.
5//!
6//! Each impl below is a `todo!("phase-d: trace X")` stub. The
7//! panic-driven mega-loop surfaces each one when a runtime path triggers
8//! `Heap::full_collect`. Each agent works on ONE type — no family
9//! expansion (Trace impls have subtle invariants).
10//!
11//! Implementation guidance for agents:
12//!   1. Read the type definition; enumerate every field
13//!   2. For every `Gc<T>`, `GcRef<T>`, or container (Vec/Option/HashMap)
14//!      thereof, call `m.mark(field)` or `field.trace(m)` appropriately
15//!   3. Skip non-GC fields (primitives, `String`, `Vec<u8>`)
16//!   4. Skip "intentionally not traced" fields (weak refs)
17//!   5. Reference `reference/lua-5.4.7/src/lgc.c`'s `reallymarkobject`
18
19use lua_gc::{Marker, Trace};
20use crate::state::{LuaState, GlobalState};
21use crate::string::{LuaStringImpl, LuaUserDataImpl};
22use lua_types::{LuaClosure, LuaValue};
23
24/// Phase-B internal richer LuaString. The byte buffer is a Rust `Rc<[u8]>`
25/// (not GC-managed); no fields to mark.
26impl Trace for LuaStringImpl {
27    fn trace(&self, _m: &mut Marker) {}
28}
29
30/// Phase-B internal userdata. Both `metatable` and `uv` are currently
31/// `Option<()>` / `Vec<()>` stubs — no GC edges to walk yet. Becomes
32/// real when userdata machinery lands post-D-1.
33impl Trace for LuaUserDataImpl {
34    fn trace(&self, _m: &mut Marker) {}
35}
36
37impl Trace for LuaState {
38    fn trace(&self, m: &mut Marker) {
39        // and the open-upvalue list. Trace frame-bounded live ranges instead of
40        // every slot up to `ci.top`: that reserved tail can contain stale values
41        // from previous calls. Lua locals that sit above the transient `top` are
42        // added explicitly from debug local metadata.
43        let trace_debug_locals = self.cached_thread_id == self.global.borrow().current_thread_id;
44        let mut ci_idx = Some(self.ci);
45        while let Some(idx) = ci_idx {
46            let ci = &self.call_info[idx.as_usize()];
47            let start = ci.func.0 as usize;
48            let end_idx = if idx == self.ci {
49                self.top.0 as usize
50            } else if let Some(next) = ci.next {
51                self.call_info[next.as_usize()].func.0 as usize
52            } else {
53                self.top.0 as usize
54            };
55            let end = end_idx.min(self.stack.len());
56            if start < end {
57                for slot in &self.stack[start..end] {
58                    slot.val.trace(m);
59                }
60            }
61            if trace_debug_locals && ci.is_lua() {
62                if let Some(slot) = self.stack.get(ci.func.0 as usize) {
63                    if let LuaValue::Function(LuaClosure::Lua(cl)) = &slot.val {
64                        let pc = ci.saved_pc().saturating_sub(1) as i32;
65                        let base = ci.func.0 as usize + 1;
66                        let mut n = 1i32;
67                        while crate::func::get_local_name(&cl.proto, n, pc).is_some() {
68                            let idx = base + (n as usize - 1);
69                            if let Some(local_slot) = self.stack.get(idx) {
70                                local_slot.val.trace(m);
71                            }
72                            n += 1;
73                        }
74                    }
75                }
76            }
77            ci_idx = ci.previous;
78        }
79
80        for uv in self.openupval.iter() {
81            uv.trace(m);
82        }
83
84        // PORT NOTE: `global` (Rc<RefCell<GlobalState>>) is reached from the
85        // heap's root via GlobalState::trace; tracing it from each thread
86        // would re-enter the root and is explicitly excluded.
87        // PORT NOTE: `call_info` entries carry pc offsets and stack indices
88        // but no direct GcRef fields. The active closure is reached through
89        // the stack slot at `ci.func`, already covered by the stack walk.
90        // PORT NOTE: `tbclist` holds StackIdx values only; the to-be-closed
91        // objects themselves live on the stack and are traced there.
92    }
93}
94
95impl Trace for GlobalState {
96    fn trace(&self, m: &mut Marker) {
97        // per-type metatables, and pending finalizers. We expand the set to
98        // include preallocated short strings (memerrmsg, tmname[]) and the
99        // open-upvalue thread list, both of which the panic-driven Phase-D
100        // mega-loop expects to see at the root.
101
102        self.l_registry.trace(m);
103
104        // Values held by Rust-side embedding handles are rooted outside the
105        // Lua registry table so handle Drop can unroot without touching the
106        // Lua stack/API. They are still ordinary GC roots during marking.
107        for value in self.external_roots.iter_values() {
108            value.trace(m);
109        }
110
111        // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
112        // storage-less, so `globals` and `loaded` cannot live inside the registry
113        // table (see `init_registry`). They are kept as direct GlobalState fields
114        // and must be traced explicitly as roots; once the placeholder reconciles
115        // with vm::LuaTable, these become reachable via `l_registry` and the two
116        // lines below disappear.
117        self.globals.trace(m);
118        self.loaded.trace(m);
119
120        if let Some(t) = &self.mainthread {
121            t.trace(m);
122        }
123
124        self.main_thread_value.trace(m);
125
126        if self.current_thread_id != self.main_thread_id {
127            if let Some(entry) = self.threads.get(&self.current_thread_id) {
128                entry.value.trace(m);
129            }
130        }
131
132        // Registered coroutines are not roots by registration alone. The
133        // post-mark hook traces stacks only for thread handles that were
134        // reached from a real root, matching Lua's collectable coroutine
135        // semantics.
136
137        for slot in self.mt.iter() {
138            if let Some(t) = slot {
139                t.trace(m);
140            }
141        }
142
143        for s in self.tmname.iter() {
144            s.trace(m);
145        }
146
147        self.memerrmsg.trace(m);
148
149        for th in self.twups.iter() {
150            th.trace(m);
151        }
152
153        // The short-string intern cache holds `GcRef<LuaString>` values that
154        // callers (parser, stdlib) reuse by pointer-equality across
155        // `intern_str` calls. C-Lua treats this as a weak table cleared during
156        // the atomic weak-table pass (`clearbykeys`); we have no incremental
157        // weak-sweep yet, so leaving these untraced would leave the HashMap
158        // with dangling `Gc<LuaString>` entries after the very next collect.
159        // Trace them as strong roots until the weak-sweep machinery lands.
160        for s in self.interned_lt.values() {
161            s.trace(m);
162        }
163        for row in self.strcache.iter() {
164            for s in row.iter() {
165                s.trace(m);
166            }
167        }
168
169        // Do not trace `gc_tracked_long_strings` here. That vector is memory
170        // accounting metadata, not an owning root. Lua C treats strings as
171        // non-weak only when they are reached through a surviving table entry
172        // (`iscleared` marks them during weak cleanup); our post-mark weak pass
173        // mirrors that by marking string keys/values returned from
174        // `prune_weak_dead`. Rooting the whole accounting list would keep dead
175        // long strings alive and break gc.lua's weak-string-key checks.
176
177        // Pending finalizers are NOT traced here — that's what lets the mark
178        // phase distinguish "still reachable from the user program" from
179        // "only kept alive by the finalizer registry". `collect_via_heap`'s
180        // post-mark hook checks each entry against the visited set; an
181        // unvisited entry is moved to `to_be_finalized` and explicitly
182        // marked there so it survives the sweep.
183        //
184        // `to_be_finalized` IS traced as a strong root: tables in this list
185        // are awaiting their `__gc` call but are otherwise dead, and the
186        // table (plus its descendants) must survive long enough for the
187        // finalizer to run.
188        for t in self.to_be_finalized.iter() {
189            t.trace(m);
190        }
191
192        // Trace suspended parent stacks. When a coroutine is running, any
193        // parent threads are suspended and their stacks are not reachable from
194        // `threads` (which only holds coroutines, not the main thread). Before
195        // `aux_resume` resumes a coroutine it pushes a snapshot of the parent's
196        // live stack onto `suspended_parent_stacks` so those GC-managed values
197        // remain marked during collections triggered from inside the coroutine.
198        for stack_snapshot in self.suspended_parent_stacks.iter() {
199            for v in stack_snapshot.iter() {
200                v.trace(m);
201            }
202        }
203        for upval_snapshot in self.suspended_parent_open_upvals.iter() {
204            for uv in upval_snapshot.iter() {
205                uv.trace(m);
206            }
207        }
208
209        // PORT NOTE: `strt` (the internal LuaStringImpl intern table) is a
210        // weak table in C; entries are cleared during the atomic weak-table
211        // pass (`clearbykeys`), not marked as roots. The current port has no
212        // incremental weak-sweep, but `strt` is keyed by byte-content rather
213        // than by `Gc` identity, so a dangling entry there is silently
214        // recreated by the next `intern_str` — no UAF, unlike `interned_lt`.
215        // PORT NOTE: `fixedgc` holds objects pre-marked fixed/black at
216        // allocation (`luaC_fix`); the mark phase never re-visits them, and
217        // `dyn Collectable` does not implement `Trace` here.
218        // PORT NOTE: `allgc`, `finobj`, `gray`, `grayagain`, `tobefnz`,
219        // `weak`, `ephemeron`, `allweak` are GC bookkeeping lists owned by
220        // `heap` — they are the universe of allocated objects, not roots.
221    }
222}
223
224// ──────────────────────────────────────────────────────────────────────────────
225// PORT STATUS
226//   source:        n/a (GC Trace impls bridging lua-vm and lua-gc)
227//   target_crate:  lua-vm
228//   confidence:    high
229//   todos:         0
230//   port_notes:    0
231//   unsafe_blocks: 0
232//   notes:         Implements lua_gc::Trace for LuaState + GlobalState. C does this via
233//                  hand-written mark routines in lgc.c; we use a trait dispatch.
234// ──────────────────────────────────────────────────────────────────────────────