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