Skip to main content

cljrs_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, GlobalEnv};
14
15// ── Thread-local context stack ───────────────────────────────────────────────
16
17struct EvalContext {
18    globals: Arc<GlobalEnv>,
19    current_ns: Arc<str>,
20}
21
22thread_local! {
23    static EVAL_CONTEXT: RefCell<Vec<EvalContext>> = const { RefCell::new(Vec::new()) };
24}
25
26/// Push the current eval context before calling a native function.
27pub fn push_eval_context(env: &Env) {
28    EVAL_CONTEXT.with(|stack| {
29        stack.borrow_mut().push(EvalContext {
30            globals: env.globals.clone(),
31            current_ns: env.current_ns.clone(),
32        });
33    });
34}
35
36/// Pop the eval context after a native function returns.
37pub fn pop_eval_context() {
38    EVAL_CONTEXT.with(|stack| {
39        stack.borrow_mut().pop();
40    });
41}
42
43/// Capture the current eval context so it can be installed on another thread.
44///
45/// Returns `None` if there is no active context.
46pub fn capture_eval_context() -> Option<(Arc<GlobalEnv>, Arc<str>)> {
47    EVAL_CONTEXT.with(|stack| {
48        let s = stack.borrow();
49        let ec = s.last()?;
50        Some((ec.globals.clone(), ec.current_ns.clone()))
51    })
52}
53
54/// Install a previously captured eval context on the current thread.
55///
56/// Call this at the start of a spawned thread so that `invoke` works.
57pub fn install_eval_context(globals: Arc<GlobalEnv>, ns: Arc<str>) {
58    EVAL_CONTEXT.with(|stack| {
59        stack.borrow_mut().push(EvalContext {
60            globals,
61            current_ns: ns,
62        });
63    });
64}
65
66// ── Public API ───────────────────────────────────────────────────────────────
67
68/// Call a Clojure-callable `Value` with the given arguments.
69///
70/// This can be called from any Rust code running inside an active evaluation
71/// (i.e., inside a builtin function, a `Thunk::force`, etc.).
72///
73/// # Errors
74///
75/// Returns `Err` if called outside an eval context, or if the callee raises
76/// an error.
77/// Execute a closure with access to a temporary `Env` constructed from the
78/// current eval context.
79///
80/// This is used by the IR interpreter for calling builtins that need an `Env`
81/// (e.g., for nested `apply_value` calls from inside a `NativeFunction`
82/// closure).
83///
84/// # Errors
85///
86/// Returns `Err` if called outside an eval context.
87pub fn with_eval_context<F, R>(f: F) -> Result<R, crate::error::EvalError>
88where
89    F: FnOnce(&mut Env) -> Result<R, crate::error::EvalError>,
90{
91    let (globals, ns) = EVAL_CONTEXT.with(|stack| {
92        let s = stack.borrow();
93        let ec = s.last().ok_or_else(|| {
94            crate::error::EvalError::Runtime(
95                "with_eval_context called outside eval context".to_string(),
96            )
97        })?;
98        Ok::<_, crate::error::EvalError>((ec.globals.clone(), ec.current_ns.clone()))
99    })?;
100    let mut env = Env::new(globals, &ns);
101    f(&mut env)
102}
103
104pub fn invoke(f: &Value, args: Vec<Value>) -> ValueResult<Value> {
105    let (globals, ns) = EVAL_CONTEXT.with(|stack| {
106        let s = stack.borrow();
107        let ec = s
108            .last()
109            .ok_or_else(|| ValueError::Other("invoke called outside eval context".into()))?;
110        Ok((ec.globals.clone(), ec.current_ns.clone()))
111    })?;
112    let mut env = Env::new(globals, &ns);
113    // Fast path for Clojure functions: call directly through the GlobalEnv
114    // function pointer, bypassing the large apply_value stack frame.
115    // Unwrap metadata so a WithMeta-wrapped fn is callable.
116    let f = f.unwrap_meta();
117    let result = if let Value::Fn(cljx_fn) = f {
118        env.call_cljrs_fn(cljx_fn.get(), &args)
119    } else {
120        crate::apply::apply_value(f, args, &mut env)
121    };
122    result.map_err(|e| match e {
123        crate::error::EvalError::Thrown(v) => ValueError::Thrown(v),
124        other => ValueError::Other(format!("{other}")),
125    })
126}