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}