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