Skip to main content

cljrs_runtime/env/
callback.rs

1//! Thread-local eval context for Rust→Clojure callbacks.
2//!
3//! When a native (Rust) builtin needs to call a Clojure function — for example,
4//! a comparator passed to `sort-by` — it can use [`invoke`] to do so.  The eval
5//! context is pushed automatically before every native function call and popped
6//! afterward, so `invoke` is always available inside builtins.
7
8use std::cell::RefCell;
9use std::sync::Arc;
10
11use cljrs_value::{Value, ValueError, ValueResult};
12
13use crate::env::env::{Env, GlobalEnv};
14
15// ── Thread-local context stack ───────────────────────────────────────────────
16
17struct EvalContext {
18    globals: Arc<GlobalEnv>,
19    current_ns: Arc<str>,
20    /// True when the active call originates from inside an `^:async` function
21    /// body. Lets blocking builtins (`deref`) reject use that should be `await`.
22    is_async: bool,
23}
24
25thread_local! {
26    static EVAL_CONTEXT: RefCell<Vec<EvalContext>> = const { RefCell::new(Vec::new()) };
27}
28cljrs_gc::mutator_slot!("cljrs-runtime/EVAL_CONTEXT", refcell EVAL_CONTEXT: Vec<EvalContext>, trace trace_saved);
29
30/// Push the current eval context before calling a native function.
31pub fn push_eval_context(env: &Env) {
32    EVAL_CONTEXT.with(|stack| {
33        stack.borrow_mut().push(EvalContext {
34            globals: env.globals.clone(),
35            current_ns: env.current_ns.clone(),
36            is_async: env.is_async,
37        });
38    });
39}
40
41pub(crate) fn scoped_eval_context(env: &Env) -> EvalContextGuard {
42    push_eval_context(env);
43    EvalContextGuard { _priv: () }
44}
45
46/// True when the innermost active eval context is inside an `^:async` function
47/// body. Returns `false` when there is no active context.
48pub fn current_is_async() -> bool {
49    EVAL_CONTEXT.with(|stack| stack.borrow().last().is_some_and(|ec| ec.is_async))
50}
51
52/// Pop the eval context after a native function returns.
53pub fn pop_eval_context() {
54    EVAL_CONTEXT.with(|stack| {
55        stack.borrow_mut().pop();
56    });
57}
58
59/// Capture the current eval context so it can be installed on another thread.
60///
61/// Returns `None` if there is no active context.
62pub fn capture_eval_context() -> Option<(Arc<GlobalEnv>, Arc<str>)> {
63    EVAL_CONTEXT.with(|stack| {
64        let s = stack.borrow();
65        let ec = s.last()?;
66        Some((ec.globals.clone(), ec.current_ns.clone()))
67    })
68}
69
70/// Install a previously captured eval context on the current thread.
71///
72/// Call this at the start of a spawned thread so that `invoke` works.
73pub fn install_eval_context(globals: Arc<GlobalEnv>, ns: Arc<str>) {
74    EVAL_CONTEXT.with(|stack| {
75        stack.borrow_mut().push(EvalContext {
76            globals,
77            current_ns: ns,
78            // Cross-thread installs (agent/future worker threads) run blocking,
79            // synchronous work — never an async-yielding context.
80            is_async: false,
81        });
82    });
83}
84
85/// RAII guard that pops one eval context on drop (including on unwind).
86///
87/// Returned by [`install_eval_context_guard`]; use it when the push and pop
88/// must stay balanced across early returns or panics.
89pub struct EvalContextGuard {
90    _priv: (),
91}
92
93impl Drop for EvalContextGuard {
94    fn drop(&mut self) {
95        pop_eval_context();
96    }
97}
98
99/// Like [`install_eval_context`], but returns a guard that pops the context
100/// when dropped.
101///
102/// Used by the JIT-native dispatch seam: native code resolves globals and
103/// calls function values through rt_abi bridges that all require an eval
104/// context on the calling thread.
105pub fn install_eval_context_guard(globals: Arc<GlobalEnv>, ns: Arc<str>) -> EvalContextGuard {
106    install_eval_context(globals, ns);
107    EvalContextGuard { _priv: () }
108}
109
110// ── Public API ───────────────────────────────────────────────────────────────
111
112/// Call a Clojure-callable `Value` with the given arguments.
113///
114/// This can be called from any Rust code running inside an active evaluation
115/// (i.e., inside a builtin function, a `Thunk::force`, etc.).
116///
117/// # Errors
118///
119/// Returns `Err` if called outside an eval context, or if the callee raises
120/// an error.
121/// Execute a closure with access to a temporary `Env` constructed from the
122/// current eval context.
123///
124/// This is used by the IR interpreter for calling builtins that need an `Env`
125/// (e.g., for nested `apply_value` calls from inside a `NativeFunction`
126/// closure).
127///
128/// # Errors
129///
130/// Returns `Err` if called outside an eval context.
131pub fn with_eval_context<F, R>(f: F) -> Result<R, crate::env::error::EvalError>
132where
133    F: FnOnce(&mut Env) -> Result<R, crate::env::error::EvalError>,
134{
135    let (globals, ns) = EVAL_CONTEXT.with(|stack| {
136        let s = stack.borrow();
137        let ec = s.last().ok_or_else(|| {
138            crate::env::error::EvalError::Runtime(
139                "with_eval_context called outside eval context".to_string(),
140            )
141        })?;
142        Ok::<_, crate::env::error::EvalError>((ec.globals.clone(), ec.current_ns.clone()))
143    })?;
144    let mut env = Env::new(globals, &ns);
145    f(&mut env)
146}
147
148pub fn invoke(f: &Value, args: Vec<Value>) -> ValueResult<Value> {
149    let (globals, ns) = EVAL_CONTEXT.with(|stack| {
150        let s = stack.borrow();
151        let ec = s
152            .last()
153            .ok_or_else(|| ValueError::Other("invoke called outside eval context".into()))?;
154        Ok((ec.globals.clone(), ec.current_ns.clone()))
155    })?;
156    let mut env = Env::new(globals, &ns);
157    // Fast path for Clojure functions: call directly through the GlobalEnv
158    // function pointer, bypassing the large apply_value stack frame.
159    // Unwrap metadata so a WithMeta-wrapped fn is callable.
160    let f = f.unwrap_meta();
161    let result = if let Value::Fn(cljx_fn) = f {
162        // Honor `^:async` dispatch (spawn the body, return a Future) just like
163        // `apply_value` — otherwise a compiled/native caller invoking an async
164        // fn would run its body synchronously and never get a Future.
165        if let Some(fut) = crate::env::apply::dispatch_if_async(f, &args, &env) {
166            Ok(fut)
167        } else {
168            env.call_cljrs_fn(cljx_fn.get(), &args)
169        }
170    } else {
171        crate::env::apply::apply_value(f, args, &mut env)
172    };
173    result.map_err(|e| match e {
174        crate::env::error::EvalError::Thrown(v) => ValueError::Thrown(v),
175        crate::env::error::EvalError::GasExhausted => ValueError::GasExhausted,
176        crate::env::error::EvalError::MemoryLimitExceeded(fault) => {
177            // Compiled callers drop most errors on the floor; keep the
178            // fault in the runtime-fault slot so the compiled-code entry
179            // seam still reports it.
180            cljrs_gc::governor::confirm_runtime_fault(fault);
181            ValueError::MemoryLimitExceeded(fault)
182        }
183        other => ValueError::Other(format!("{other}")),
184    })
185}
186
187fn trace_saved(contexts: &Vec<EvalContext>, visitor: &mut cljrs_gc::MarkVisitor) {
188    for context in contexts {
189        crate::env::gc_roots::trace_globals(&context.globals, visitor);
190    }
191}