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}
28
29/// Push the current eval context before calling a native function.
30pub fn push_eval_context(env: &Env) {
31 EVAL_CONTEXT.with(|stack| {
32 stack.borrow_mut().push(EvalContext {
33 globals: env.globals.clone(),
34 current_ns: env.current_ns.clone(),
35 is_async: env.is_async,
36 });
37 });
38}
39
40/// True when the innermost active eval context is inside an `^:async` function
41/// body. Returns `false` when there is no active context.
42pub fn current_is_async() -> bool {
43 EVAL_CONTEXT.with(|stack| stack.borrow().last().is_some_and(|ec| ec.is_async))
44}
45
46/// Pop the eval context after a native function returns.
47pub fn pop_eval_context() {
48 EVAL_CONTEXT.with(|stack| {
49 stack.borrow_mut().pop();
50 });
51}
52
53/// Capture the current eval context so it can be installed on another thread.
54///
55/// Returns `None` if there is no active context.
56pub fn capture_eval_context() -> Option<(Arc<GlobalEnv>, Arc<str>)> {
57 EVAL_CONTEXT.with(|stack| {
58 let s = stack.borrow();
59 let ec = s.last()?;
60 Some((ec.globals.clone(), ec.current_ns.clone()))
61 })
62}
63
64/// Install a previously captured eval context on the current thread.
65///
66/// Call this at the start of a spawned thread so that `invoke` works.
67pub fn install_eval_context(globals: Arc<GlobalEnv>, ns: Arc<str>) {
68 EVAL_CONTEXT.with(|stack| {
69 stack.borrow_mut().push(EvalContext {
70 globals,
71 current_ns: ns,
72 // Cross-thread installs (agent/future worker threads) run blocking,
73 // synchronous work — never an async-yielding context.
74 is_async: false,
75 });
76 });
77}
78
79/// RAII guard that pops one eval context on drop (including on unwind).
80///
81/// Returned by [`install_eval_context_guard`]; use it when the push and pop
82/// must stay balanced across early returns or panics.
83pub struct EvalContextGuard {
84 _priv: (),
85}
86
87impl Drop for EvalContextGuard {
88 fn drop(&mut self) {
89 pop_eval_context();
90 }
91}
92
93/// Like [`install_eval_context`], but returns a guard that pops the context
94/// when dropped.
95///
96/// Used by the JIT-native dispatch seam: native code resolves globals and
97/// calls function values through rt_abi bridges that all require an eval
98/// context on the calling thread.
99pub fn install_eval_context_guard(globals: Arc<GlobalEnv>, ns: Arc<str>) -> EvalContextGuard {
100 install_eval_context(globals, ns);
101 EvalContextGuard { _priv: () }
102}
103
104// ── Public API ───────────────────────────────────────────────────────────────
105
106/// Call a Clojure-callable `Value` with the given arguments.
107///
108/// This can be called from any Rust code running inside an active evaluation
109/// (i.e., inside a builtin function, a `Thunk::force`, etc.).
110///
111/// # Errors
112///
113/// Returns `Err` if called outside an eval context, or if the callee raises
114/// an error.
115/// Execute a closure with access to a temporary `Env` constructed from the
116/// current eval context.
117///
118/// This is used by the IR interpreter for calling builtins that need an `Env`
119/// (e.g., for nested `apply_value` calls from inside a `NativeFunction`
120/// closure).
121///
122/// # Errors
123///
124/// Returns `Err` if called outside an eval context.
125pub fn with_eval_context<F, R>(f: F) -> Result<R, crate::env::error::EvalError>
126where
127 F: FnOnce(&mut Env) -> Result<R, crate::env::error::EvalError>,
128{
129 let (globals, ns) = EVAL_CONTEXT.with(|stack| {
130 let s = stack.borrow();
131 let ec = s.last().ok_or_else(|| {
132 crate::env::error::EvalError::Runtime(
133 "with_eval_context called outside eval context".to_string(),
134 )
135 })?;
136 Ok::<_, crate::env::error::EvalError>((ec.globals.clone(), ec.current_ns.clone()))
137 })?;
138 let mut env = Env::new(globals, &ns);
139 f(&mut env)
140}
141
142pub fn invoke(f: &Value, args: Vec<Value>) -> ValueResult<Value> {
143 let (globals, ns) = EVAL_CONTEXT.with(|stack| {
144 let s = stack.borrow();
145 let ec = s
146 .last()
147 .ok_or_else(|| ValueError::Other("invoke called outside eval context".into()))?;
148 Ok((ec.globals.clone(), ec.current_ns.clone()))
149 })?;
150 let mut env = Env::new(globals, &ns);
151 // Fast path for Clojure functions: call directly through the GlobalEnv
152 // function pointer, bypassing the large apply_value stack frame.
153 // Unwrap metadata so a WithMeta-wrapped fn is callable.
154 let f = f.unwrap_meta();
155 let result = if let Value::Fn(cljx_fn) = f {
156 // Honor `^:async` dispatch (spawn the body, return a Future) just like
157 // `apply_value` — otherwise a compiled/native caller invoking an async
158 // fn would run its body synchronously and never get a Future.
159 if let Some(fut) = crate::env::apply::dispatch_if_async(f, &args, &env) {
160 Ok(fut)
161 } else {
162 env.call_cljrs_fn(cljx_fn.get(), &args)
163 }
164 } else {
165 crate::env::apply::apply_value(f, args, &mut env)
166 };
167 result.map_err(|e| match e {
168 crate::env::error::EvalError::Thrown(v) => ValueError::Thrown(v),
169 crate::env::error::EvalError::GasExhausted => ValueError::GasExhausted,
170 other => ValueError::Other(format!("{other}")),
171 })
172}