cljrs_runtime/env/gc_roots.rs
1#[cfg(not(feature = "no-gc"))]
2use crate::env::dynamics;
3use crate::env::env::Env;
4#[cfg(not(feature = "no-gc"))]
5use crate::env::env::GlobalEnv;
6use std::cell::RefCell;
7
8// ── Stop-the-world reclaim hooks (JIT code unloading, cold-IR sweep) ────────
9//
10// Reclamation of execution-engine caches runs only at a stop-the-world
11// safepoint, when every mutator thread is parked and active JIT frames can be
12// scanned safely. GC collection is the existing STW point, so interested
13// tiers install hooks here that run at the tail of every collection while the
14// STW guard is still held. Current registrants: the compiler's JIT code cache
15// (`cljrs_compiler::jit::code_cache`, superseded native modules) and this
16// crate's lowering worker (idle Tier-1 IR, Phase 10.7).
17
18type StwReclaimHook = Box<dyn Fn() + Send + Sync + 'static>;
19static STW_RECLAIM_HOOKS: std::sync::RwLock<Vec<StwReclaimHook>> =
20 std::sync::RwLock::new(Vec::new());
21
22/// Register a stop-the-world reclaim hook. Multiple hooks may be registered;
23/// each runs at every STW point, in registration order.
24///
25/// Hooks run inside the STW guard after each collection, so they may assume
26/// all other mutator threads are parked.
27pub fn set_stw_reclaim_hook(f: impl Fn() + Send + Sync + 'static) {
28 STW_RECLAIM_HOOKS.write().unwrap().push(Box::new(f));
29}
30
31/// Run the STW reclaim hooks, if any. Caller must hold the STW guard.
32#[cfg(not(feature = "no-gc"))]
33fn run_stw_reclaim() {
34 for hook in STW_RECLAIM_HOOKS.read().unwrap().iter() {
35 hook();
36 }
37}
38
39// ── Thread-local Env root registry ──────────────────────────────────────────
40//
41// When the interpreter enters a function call, the caller's Env stays on the
42// Rust stack but the callee creates a fresh Env. If GC triggers inside the
43// callee, only the callee's Env is passed to `gc_safepoint`. To keep the
44// caller's local bindings alive we maintain a thread-local stack of pointers
45// to all active Envs on this thread's call stack.
46//
47// SAFETY: the raw pointers are valid during STW collection because:
48// - The collecting thread's own Envs are in earlier (still-live) stack frames.
49// - Other threads are parked at safepoints; their stacks (and Envs) are frozen.
50
51thread_local! {
52 static ENV_ROOTS: RefCell<Vec<*const Env>> = const { RefCell::new(Vec::new()) };
53 /// Shadow stack of Value pointers on the Rust call stack that need to
54 /// survive GC. Each entry is a `(ptr, count)` pair pointing to a
55 /// contiguous slice of Values (e.g., a Vec's backing storage or a single
56 /// Value on the stack).
57 static VALUE_ROOTS: RefCell<Vec<(*const cljrs_value::Value, usize)>> =
58 const { RefCell::new(Vec::new()) };
59 /// Shadow stack for `Option<Value>` slices (e.g., the IR interpreter's
60 /// register file). Each entry is `(ptr, count)` pointing to a fixed-size
61 /// heap slice whose address will not change for the lifetime of the entry.
62 static OPTION_VALUE_ROOTS: RefCell<Vec<(*const Option<cljrs_value::Value>, usize)>> =
63 const { RefCell::new(Vec::new()) };
64}
65
66/// RAII guard that pops the Env pointer on drop.
67pub struct EnvRootGuard;
68
69impl Drop for EnvRootGuard {
70 fn drop(&mut self) {
71 ENV_ROOTS.with(|roots| {
72 roots.borrow_mut().pop();
73 });
74 }
75}
76
77/// RAII guard that pops one entry from the value shadow stack on drop.
78pub struct ValueRootGuard {
79 pushed: bool,
80}
81
82impl Drop for ValueRootGuard {
83 fn drop(&mut self) {
84 if self.pushed {
85 VALUE_ROOTS.with(|roots| {
86 roots.borrow_mut().pop();
87 });
88 }
89 }
90}
91
92/// Register an Env as a GC root for the duration of its use.
93/// Returns a guard that unregisters on drop.
94pub fn push_env_root(env: &Env) -> EnvRootGuard {
95 ENV_ROOTS.with(|roots| {
96 roots.borrow_mut().push(env as *const Env);
97 });
98 EnvRootGuard
99}
100
101/// Register a single Value as a GC root.
102pub fn root_value(val: &cljrs_value::Value) -> ValueRootGuard {
103 VALUE_ROOTS.with(|roots| {
104 roots
105 .borrow_mut()
106 .push((val as *const cljrs_value::Value, 1));
107 });
108 ValueRootGuard { pushed: true }
109}
110
111/// Register a slice of Values as GC roots (e.g., a Vec<Value>).
112pub fn root_values(vals: &[cljrs_value::Value]) -> ValueRootGuard {
113 if vals.is_empty() {
114 return ValueRootGuard { pushed: false };
115 }
116 VALUE_ROOTS.with(|roots| {
117 roots.borrow_mut().push((vals.as_ptr(), vals.len()));
118 });
119 ValueRootGuard { pushed: true }
120}
121
122/// RAII guard that pops one entry from the option-value shadow stack on drop.
123pub struct OptionValueRootGuard {
124 pushed: bool,
125}
126
127impl Drop for OptionValueRootGuard {
128 fn drop(&mut self) {
129 if self.pushed {
130 OPTION_VALUE_ROOTS.with(|roots| {
131 roots.borrow_mut().pop();
132 });
133 }
134 }
135}
136
137/// Register a slice of `Option<Value>` as GC roots.
138///
139/// The caller **must** ensure the slice's heap address is stable for the
140/// lifetime of the returned guard — use `Box<[Option<Value>]>` rather than
141/// a `Vec` that could reallocate.
142pub fn root_option_values(vals: &[Option<cljrs_value::Value>]) -> OptionValueRootGuard {
143 if vals.is_empty() {
144 return OptionValueRootGuard { pushed: false };
145 }
146 OPTION_VALUE_ROOTS.with(|roots| {
147 roots.borrow_mut().push((vals.as_ptr(), vals.len()));
148 });
149 OptionValueRootGuard { pushed: true }
150}
151
152/// Force an immediate GC collection, bypassing the memory-pressure threshold.
153///
154/// Unlike `gc_safepoint`, this always initiates collection regardless of
155/// `gc_requested()`. Use this after removing namespaces from globals to ensure
156/// their closures and form-trees are freed before the next namespace is loaded.
157///
158/// Under `no-gc` this is a no-op.
159#[cfg(feature = "no-gc")]
160pub fn force_collect(_env: &Env) {}
161
162#[cfg(not(feature = "no-gc"))]
163pub fn force_collect(env: &Env) {
164 let Some(_stw_guard) = cljrs_gc::begin_stw() else {
165 // Another thread is already collecting — just wait for it.
166 cljrs_gc::safepoint();
167 return;
168 };
169
170 cljrs_gc::HEAP.collect(|visitor| {
171 cljrs_gc::HEAP.trace_registered_roots(visitor);
172 trace_env_roots(env, visitor);
173 trace_thread_env_roots(visitor);
174 trace_value_roots(visitor);
175 trace_option_value_roots(visitor);
176 dynamics::trace_current(visitor);
177 crate::env::taps::trace_roots(visitor);
178 cljrs_gc::trace_thread_alloc_roots(visitor);
179 });
180 // Reclaim superseded JIT code while the world is still stopped.
181 run_stw_reclaim();
182}
183
184/// Interpreter-level GC safepoint.
185///
186/// Under `no-gc` this is a no-op. Under GC mode it either parks (if collection
187/// is in progress) or initiates a collection (if memory pressure was signalled).
188#[cfg(feature = "no-gc")]
189pub fn gc_safepoint(_env: &Env) {}
190
191#[cfg(not(feature = "no-gc"))]
192pub fn gc_safepoint(env: &Env) {
193 // Fast path: no GC activity at all.
194 if !cljrs_gc::gc_requested() && !cljrs_gc::CONFIG_CANCELLATION.in_progress() {
195 return;
196 }
197
198 // If a GC is already in progress (another thread is collecting), just park.
199 if cljrs_gc::CONFIG_CANCELLATION.in_progress() {
200 cljrs_gc::safepoint();
201 return;
202 }
203
204 // A GC was requested (memory pressure). Try to become the collector.
205 if !cljrs_gc::take_gc_request() {
206 // Another thread took the request; if collection started, park.
207 cljrs_gc::safepoint();
208 return;
209 }
210
211 // We won the request. Initiate STW collection.
212 let Some(_stw_guard) = cljrs_gc::begin_stw() else {
213 // Race: another thread started collecting between our take and begin.
214 cljrs_gc::safepoint();
215 return;
216 };
217
218 // All other threads are now parked. Collect with registered roots
219 // plus ALL of this thread's active environments and dynamic bindings.
220 cljrs_gc::HEAP.collect(|visitor| {
221 // Trace globally registered roots (GlobalEnv, etc.)
222 cljrs_gc::HEAP.trace_registered_roots(visitor);
223 // Trace the current (innermost) env
224 trace_env_roots(env, visitor);
225 // Trace all caller Envs registered on this thread's stack
226 trace_thread_env_roots(visitor);
227 // Trace values on the Rust call stack (shadow stack)
228 trace_value_roots(visitor);
229 // Trace Option<Value> slices (e.g. IR interpreter register files)
230 trace_option_value_roots(visitor);
231 // Trace dynamic variable bindings on this thread
232 dynamics::trace_current(visitor);
233 // Trace the global tap system (functions and queued values)
234 crate::env::taps::trace_roots(visitor);
235 // Trace in-flight allocations from this thread's alloc root frames
236 cljrs_gc::trace_thread_alloc_roots(visitor);
237 });
238 // Reclaim superseded JIT code while the world is still stopped.
239 run_stw_reclaim();
240 // _stw_guard drop clears in_progress, waking parked threads.
241}
242
243// ── GC-only root tracing helpers ─────────────────────────────────────────────
244
245/// Trace all GcPtr values reachable from an Env's local frames.
246#[cfg(not(feature = "no-gc"))]
247fn trace_env_roots(env: &Env, visitor: &mut cljrs_gc::MarkVisitor) {
248 use cljrs_gc::Trace;
249 // Trace local frame bindings
250 for frame in &env.frames {
251 for (_name, val) in &frame.bindings {
252 val.trace(visitor);
253 }
254 }
255 // Trace the globals (namespaces, vars) — these are also registered
256 // as root tracers, but it's safe to trace twice (idempotent marking).
257 trace_globals(&env.globals, visitor);
258}
259
260/// Trace all Values registered in the thread-local value shadow stack.
261#[cfg(not(feature = "no-gc"))]
262fn trace_value_roots(visitor: &mut cljrs_gc::MarkVisitor) {
263 use cljrs_gc::Trace;
264 VALUE_ROOTS.with(|roots| {
265 for &(ptr, count) in roots.borrow().iter() {
266 // SAFETY: pointers are valid — they point to Values on this thread's
267 // still-live stack frames or heap-allocated Vecs whose owners are
268 // on still-live stack frames.
269 let slice = unsafe { std::slice::from_raw_parts(ptr, count) };
270 for val in slice {
271 val.trace(visitor);
272 }
273 }
274 });
275}
276
277/// Trace all Option<Value> slices registered in the thread-local shadow stack.
278///
279/// Used for the IR interpreter's register file (a `Box<[Option<Value>]>`).
280#[cfg(not(feature = "no-gc"))]
281fn trace_option_value_roots(visitor: &mut cljrs_gc::MarkVisitor) {
282 use cljrs_gc::Trace;
283 OPTION_VALUE_ROOTS.with(|roots| {
284 for &(ptr, count) in roots.borrow().iter() {
285 // SAFETY: the slice is a Box<[Option<Value>]> owned by an active
286 // stack frame; the address is stable for the guard's lifetime.
287 let slice = unsafe { std::slice::from_raw_parts(ptr, count) };
288 for val in slice.iter().flatten() {
289 val.trace(visitor);
290 }
291 }
292 });
293}
294
295/// Trace all Envs registered in the thread-local root stack.
296#[cfg(not(feature = "no-gc"))]
297fn trace_thread_env_roots(visitor: &mut cljrs_gc::MarkVisitor) {
298 use cljrs_gc::Trace;
299 ENV_ROOTS.with(|roots| {
300 for env_ptr in roots.borrow().iter() {
301 // SAFETY: pointers are valid — they point to Envs on this thread's
302 // still-live stack frames (we are the collector, so our stack is active).
303 let env = unsafe { &**env_ptr };
304 for frame in &env.frames {
305 for (_name, val) in &frame.bindings {
306 val.trace(visitor);
307 }
308 }
309 }
310 });
311}
312
313/// Trace all namespaces and their contents.
314#[cfg(not(feature = "no-gc"))]
315fn trace_globals(globals: &GlobalEnv, visitor: &mut cljrs_gc::MarkVisitor) {
316 use cljrs_gc::{GcVisitor as _, Trace};
317 let namespaces = globals.namespaces.read().unwrap();
318 for ns_ptr in namespaces.values() {
319 visitor.visit(ns_ptr);
320 }
321 drop(namespaces);
322 // Values resolved at a pinned commit may live only in the version cache
323 // (e.g. native HEAD fallbacks) — without this they would be collected.
324 let version_cache = globals.version_cache.lock().unwrap();
325 for val in version_cache.values() {
326 val.trace(visitor);
327 }
328}
329
330/// Service a pending GC request from an async (LocalSet) context.
331///
332/// Safe to call from within a Tokio `LocalSet` task at any cooperative yield
333/// point: when this executes, no other tasks are polling, so thread-local root
334/// stacks (ENV_ROOTS, VALUE_ROOTS, ALLOC_ROOTS) fully describe all GcPtrs held
335/// by suspended tasks and can be scanned safely.
336///
337/// Under `no-gc` this is a no-op.
338#[cfg(feature = "no-gc")]
339pub fn async_gc_collect() {}
340
341#[cfg(not(feature = "no-gc"))]
342pub fn async_gc_collect() {
343 if !cljrs_gc::gc_requested() && !cljrs_gc::CONFIG_CANCELLATION.in_progress() {
344 return;
345 }
346 if cljrs_gc::CONFIG_CANCELLATION.in_progress() {
347 cljrs_gc::safepoint();
348 return;
349 }
350 if !cljrs_gc::take_gc_request() {
351 cljrs_gc::safepoint();
352 return;
353 }
354 let Some(_stw_guard) = cljrs_gc::begin_stw() else {
355 cljrs_gc::safepoint();
356 return;
357 };
358 cljrs_gc::HEAP.collect(|visitor| {
359 cljrs_gc::HEAP.trace_registered_roots(visitor);
360 trace_thread_env_roots(visitor);
361 trace_value_roots(visitor);
362 trace_option_value_roots(visitor);
363 dynamics::trace_current(visitor);
364 crate::env::taps::trace_roots(visitor);
365 cljrs_gc::trace_thread_alloc_roots(visitor);
366 });
367 // Reclaim superseded JIT code while the world is still stopped.
368 run_stw_reclaim();
369}