Skip to main content

cljrs_runtime/env/
dynamics.rs

1//! Thread-local dynamic variable binding stack.
2//!
3//! `binding` forms push a frame onto `BINDING_STACK` for the duration of their
4//! body; the RAII `BindingGuard` pops it on drop (handles both normal return
5//! and panics).
6
7use std::cell::RefCell;
8use std::collections::HashMap;
9
10use cljrs_gc::GcPtr;
11use cljrs_gc::Trace as _;
12use cljrs_value::{Value, Var};
13
14/// Opaque key for a Var in the binding stack (pointer identity).
15/// Stable because the GC is non-moving.
16pub type VarKey = usize;
17
18pub fn var_key_of(var: &GcPtr<Var>) -> VarKey {
19    var.get() as *const Var as usize
20}
21
22struct BindingFrame {
23    values: HashMap<VarKey, Value>,
24    #[cfg(feature = "no-gc")]
25    leases: HashMap<VarKey, cljrs_value::publish::ValueLease>,
26}
27impl BindingFrame {
28    fn new(values: HashMap<VarKey, Value>) -> Self {
29        #[cfg(feature = "no-gc")]
30        let (values, leases) = {
31            let mut output = HashMap::new();
32            let mut leases = HashMap::new();
33            for (key, value) in values {
34                let (value, lease) =
35                    cljrs_value::publish::prepare_for_owner(value, &Default::default());
36                output.insert(key, value);
37                leases.insert(key, lease);
38            }
39            (output, leases)
40        };
41        Self {
42            values,
43            #[cfg(feature = "no-gc")]
44            leases,
45        }
46    }
47}
48
49thread_local! {
50    static BINDING_STACK: RefCell<Vec<BindingFrame>> =
51        const { RefCell::new(Vec::new()) };
52}
53cljrs_gc::mutator_slot!("cljrs-runtime/BINDING_STACK", refcell BINDING_STACK: Vec<BindingFrame>, trace trace_saved);
54
55// ── RAII guard ────────────────────────────────────────────────────────────────
56
57/// Pops the innermost binding frame when dropped.
58pub struct BindingGuard;
59
60impl Drop for BindingGuard {
61    fn drop(&mut self) {
62        pop_frame();
63    }
64}
65
66// ── Stack manipulation ────────────────────────────────────────────────────────
67
68/// Push a new dynamic binding frame; return a guard that pops it on drop.
69pub fn push_frame(bindings: HashMap<VarKey, Value>) -> BindingGuard {
70    let frame = BindingFrame::new(bindings);
71    BINDING_STACK.with(|s| s.borrow_mut().push(frame));
72    BindingGuard
73}
74
75/// Pop the frame `guard` owns and hand it back, so a caller that yields between
76/// uses (the async evaluator) can keep a `binding` frame off the thread-local
77/// stack while other tasks on the same thread run, and push it again later.
78/// Must be called with the innermost frame's guard.
79pub fn take_frame(guard: BindingGuard) -> HashMap<VarKey, Value> {
80    std::mem::forget(guard);
81    BINDING_STACK.with(|s| {
82        s.borrow_mut()
83            .pop()
84            .map(|frame| {
85                #[cfg(feature = "no-gc")]
86                for lease in frame.leases.values() {
87                    lease.protect_read();
88                }
89                frame.values
90            })
91            .unwrap_or_default()
92    })
93}
94
95fn pop_frame() {
96    BINDING_STACK.with(|s| {
97        s.borrow_mut().pop();
98    });
99}
100
101// ── Lookup ────────────────────────────────────────────────────────────────────
102
103/// Check the thread-local stack first (innermost frame wins); fall back to the
104/// root binding stored in the `Var` itself.
105pub fn deref_var(var: &GcPtr<Var>) -> Option<Value> {
106    let key = var_key_of(var);
107    let tl = BINDING_STACK.with(|s| {
108        s.borrow().iter().rev().find_map(|frame| {
109            let value = frame.values.get(&key)?.clone();
110            #[cfg(feature = "no-gc")]
111            if let Some(lease) = frame.leases.get(&key) {
112                lease.protect_read();
113            }
114            Some(value)
115        })
116    });
117    tl.or_else(|| var.get().deref())
118}
119
120/// True if `var` has any thread-local binding on this thread.
121pub fn is_thread_bound(var: &GcPtr<Var>) -> bool {
122    let key = var_key_of(var);
123    BINDING_STACK.with(|s| {
124        s.borrow()
125            .iter()
126            .any(|frame| frame.values.contains_key(&key))
127    })
128}
129
130/// Set the innermost thread-local binding for `var`.
131/// Returns `false` if no thread-local binding exists (caller should fall back
132/// to setting the root).
133pub fn set_thread_local(var: &GcPtr<Var>, val: Value) -> bool {
134    let key = var_key_of(var);
135    #[cfg(feature = "no-gc")]
136    {
137        let target = BINDING_STACK.with(|stack| {
138            stack
139                .borrow()
140                .iter()
141                .rposition(|frame| frame.values.contains_key(&key))
142        });
143        let Some(index) = target else {
144            return false;
145        };
146        let (value, lease) = cljrs_value::publish::prepare_for_owner(val, &Default::default());
147        let old = BINDING_STACK.with(|stack| {
148            let mut stack = stack.borrow_mut();
149            stack[index].values.insert(key, value);
150            stack[index].leases.insert(key, lease)
151        });
152        drop(old);
153        true
154    }
155    #[cfg(not(feature = "no-gc"))]
156    BINDING_STACK.with(|s| {
157        for frame in s.borrow_mut().iter_mut().rev() {
158            if let std::collections::hash_map::Entry::Occupied(mut e) = frame.values.entry(key) {
159                e.insert(val);
160                return true;
161            }
162        }
163        false
164    })
165}
166
167// ── Binding conveyance ────────────────────────────────────────────────────────
168
169/// Snapshot the current thread's entire binding stack (for conveyance into a
170/// child thread, e.g. `future`).
171pub fn capture_current() -> Vec<HashMap<VarKey, Value>> {
172    BINDING_STACK.with(|s| {
173        s.borrow()
174            .iter()
175            .map(|frame| {
176                #[cfg(feature = "no-gc")]
177                for lease in frame.leases.values() {
178                    lease.protect_read();
179                }
180                frame.values.clone()
181            })
182            .collect()
183    })
184}
185
186/// Install a previously captured snapshot on the current (new) thread.
187pub fn install_frames(frames: Vec<HashMap<VarKey, Value>>) {
188    let frames = frames.into_iter().map(BindingFrame::new).collect();
189    BINDING_STACK.with(|s| *s.borrow_mut() = frames);
190}
191
192// ── GC root tracing ───────────────────────────────────────────────────────────
193
194/// Trace all values in the current thread's binding stack as GC roots.
195/// Call this during the GC root enumeration phase.
196pub fn trace_current(visitor: &mut cljrs_gc::MarkVisitor) {
197    BINDING_STACK.with(|s| {
198        for frame in s.borrow().iter() {
199            for val in frame.values.values() {
200                val.trace(visitor);
201            }
202        }
203    });
204}
205
206fn trace_saved(frames: &Vec<BindingFrame>, visitor: &mut cljrs_gc::MarkVisitor) {
207    for frame in frames {
208        for value in frame.values.values() {
209            value.trace(visitor);
210        }
211    }
212}