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 crate::state::{FinalizerObject, GlobalState, LuaState};
20use crate::string::{LuaStringImpl, LuaUserDataImpl};
21use lua_gc::{Marker, Trace};
22
23/// Phase-B internal richer LuaString. The byte buffer is a Rust `Rc<[u8]>`
24/// (not GC-managed); no fields to mark.
25impl Trace for LuaStringImpl {
26
27 fn type_name(&self) -> &'static str {
28 std::any::type_name::<Self>()
29 }
30
31 fn trace(&self, _m: &mut Marker) {}
32}
33
34/// Phase-B internal userdata. Both `metatable` and `uv` are currently
35/// `Option<()>` / `Vec<()>` stubs — no GC edges to walk yet. Becomes
36/// real when userdata machinery lands post-D-1.
37impl Trace for LuaUserDataImpl {
38
39 fn type_name(&self) -> &'static str {
40 std::any::type_name::<Self>()
41 }
42
43 fn trace(&self, _m: &mut Marker) {}
44}
45
46impl Trace for FinalizerObject {
47
48 fn type_name(&self) -> &'static str {
49 std::any::type_name::<Self>()
50 }
51
52 fn trace(&self, m: &mut Marker) {
53 match self {
54 FinalizerObject::Table(t) => t.trace(m),
55 FinalizerObject::UserData(u) => u.trace(m),
56 }
57 }
58}
59
60impl Trace for LuaState {
61
62 fn type_name(&self) -> &'static str {
63 std::any::type_name::<Self>()
64 }
65
66 fn trace(&self, m: &mut Marker) {
67 // C's traversethread (lgc.c) walks [stack .. top) and relies on two
68 // companion invariants this port mirrors via `gc_trace_bound` (the
69 // savestate half — widen to ci.top only for a Lua current frame)
70 // and `clear_dead_stack_tail` (the atomic-clear half, run before
71 // every collect). Every slot below the bound is therefore
72 // valid-or-nil; the old frame-bounded range walk and the saved_pc
73 // debug-local heuristic (#140 bug B's two faces) are gone.
74 let bound = self.gc_trace_bound();
75 for slot in &self.stack[..bound] {
76 slot.val.trace(m);
77 }
78
79 for uv in self.openupval.iter() {
80 uv.trace(m);
81 }
82
83 // PORT NOTE: `global` (Rc<RefCell<GlobalState>>) is reached from the
84 // heap's root via GlobalState::trace; tracing it from each thread
85 // would re-enter the root and is explicitly excluded.
86 // PORT NOTE: `call_info` entries carry pc offsets and stack indices
87 // but no direct GcRef fields. The active closure is reached through
88 // the stack slot at `ci.func`, already covered by the stack walk.
89 // PORT NOTE: `tbclist` holds StackIdx values only; the to-be-closed
90 // objects themselves live on the stack and are traced there.
91 }
92}
93
94impl Trace for GlobalState {
95
96 fn type_name(&self) -> &'static str {
97 std::any::type_name::<Self>()
98 }
99
100 fn trace(&self, m: &mut Marker) {
101 // per-type metatables, and pending finalizers. We expand the set to
102 // include preallocated short strings (memerrmsg, tmname[]) and the
103 // open-upvalue thread list, both of which the panic-driven Phase-D
104 // mega-loop expects to see at the root.
105
106 self.l_registry.trace(m);
107
108 // Values held by Rust-side embedding handles are rooted outside the
109 // Lua registry table so handle Drop can unroot without touching the
110 // Lua stack/API. They are still ordinary GC roots during marking.
111 for value in self.external_roots.iter_values() {
112 value.trace(m);
113 }
114
115 // Cross-thread open-upvalue mirrors are live roots while a coroutine
116 // resume holds the home thread's stack behind an outer mutable borrow.
117 for value in self.cross_thread_upvals.values() {
118 value.trace(m);
119 }
120
121 // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
122 // storage-less, so `globals` and `loaded` cannot live inside the registry
123 // table (see `init_registry`). They are kept as direct GlobalState fields
124 // and must be traced explicitly as roots; once the placeholder reconciles
125 // with vm::LuaTable, these become reachable via `l_registry` and the two
126 // lines below disappear.
127 self.globals.trace(m);
128 self.loaded.trace(m);
129
130 // Lua 5.1 per-thread and per-closure environments are GC roots: a
131 // coroutine's global table and a no-`_ENV`-upvalue closure's
132 // environment are reachable from the live thread / closure but stored
133 // off to the side, so they must be traced here to survive collection.
134 // Both maps are empty on 5.2–5.5, making these loops no-ops there.
135 // Dead-thread / dead-closure keys are pruned after each collection.
136 for value in self.thread_globals.values() {
137 value.trace(m);
138 }
139 for value in self.closure_envs.values() {
140 value.trace(m);
141 }
142
143 if let Some(t) = &self.mainthread {
144 t.trace(m);
145 }
146
147 self.main_thread_value.trace(m);
148
149 if self.current_thread_id != self.main_thread_id {
150 if let Some(entry) = self.threads.get(&self.current_thread_id) {
151 entry.value.trace(m);
152 }
153 }
154
155 // Registered coroutines are not roots by registration alone. The
156 // post-mark hook traces stacks only for thread handles that were
157 // reached from a real root, matching Lua's collectable coroutine
158 // semantics.
159
160 for slot in self.mt.iter() {
161 if let Some(t) = slot {
162 t.trace(m);
163 }
164 }
165
166 for s in self.tmname.iter() {
167 s.trace(m);
168 }
169
170 self.memerrmsg.trace(m);
171
172 for th in self.twups.iter() {
173 th.trace(m);
174 }
175
176 // `interned_lt` is a weak short-string cache. The collector prunes
177 // unmarked entries from the post-mark hook instead of tracing them as
178 // roots here.
179 for row in self.strcache.iter() {
180 for s in row.iter() {
181 s.trace(m);
182 }
183 }
184
185 // Pending finalizers are NOT traced here — that's what lets the mark
186 // phase distinguish "still reachable from the user program" from
187 // "only kept alive by the finalizer registry". `collect_via_heap`'s
188 // post-mark hook checks each entry against the visited set; an
189 // unvisited entry is moved to `to_be_finalized` and explicitly
190 // marked there so it survives the sweep.
191 //
192 // `to_be_finalized` IS traced as a strong root: objects in this list
193 // are awaiting their `__gc` call but are otherwise dead, and the
194 // object (plus its descendants) must survive long enough for the
195 // finalizer to run.
196 for object in self.finalizers.to_be_finalized().iter() {
197 object.trace(m);
198 }
199
200 // Trace suspended parent stacks. When a coroutine is running, any
201 // parent threads are suspended and their stacks are not reachable from
202 // `threads` (which only holds coroutines, not the main thread). Before
203 // `aux_resume` resumes a coroutine it pushes a snapshot of the parent's
204 // live stack onto `suspended_parent_stacks` so those GC-managed values
205 // remain marked during collections triggered from inside the coroutine.
206 for stack_snapshot in self.suspended_parent_stacks.iter() {
207 for v in stack_snapshot.iter() {
208 v.trace(m);
209 }
210 }
211 for upval_snapshot in self.suspended_parent_open_upvals.iter() {
212 for uv in upval_snapshot.iter() {
213 uv.trace(m);
214 }
215 }
216
217 // PORT NOTE: `strt` (the internal LuaStringImpl intern table) is a
218 // weak table in C; entries are cleared during the atomic weak-table
219 // pass (`clearbykeys`), not marked as roots. The current port has no
220 // incremental weak-sweep, but `strt` is keyed by byte-content rather
221 // than by `Gc` identity, so a dangling entry there is silently
222 // recreated by the next `intern_str` — no UAF, unlike `interned_lt`.
223 // PORT NOTE: `fixedgc` holds objects pre-marked fixed/black at
224 // allocation (`luaC_fix`); the mark phase never re-visits them, and
225 // `dyn Collectable` does not implement `Trace` here.
226 // PORT NOTE: `allgc`, `finobj`, `gray`, `grayagain`, `tobefnz`,
227 // `weak`, `ephemeron`, `allweak` are GC bookkeeping lists owned by
228 // `heap` — they are the universe of allocated objects, not roots.
229 }
230}
231
232// ──────────────────────────────────────────────────────────────────────────────
233// PORT STATUS
234// source: n/a (GC Trace impls bridging lua-vm and lua-gc)
235// target_crate: lua-vm
236// confidence: high
237// todos: 0
238// port_notes: 0
239// unsafe_blocks: 0
240// notes: Implements lua_gc::Trace for LuaState + GlobalState. C does this via
241// hand-written mark routines in lgc.c; we use a trait dispatch.
242// ──────────────────────────────────────────────────────────────────────────────