Skip to main content

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}