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