Skip to main content

cljrs_runtime/env/
apply.rs

1use crate::env::env::Env;
2use crate::env::error::{EvalError, EvalResult};
3use cljrs_value::{Arity, Value};
4use std::sync::Arc;
5
6fn check_arity(arity: &Arity, argc: usize, name: &str) -> EvalResult<()> {
7    match arity {
8        Arity::Fixed(n) if argc != *n => Err(EvalError::Arity {
9            name: name.to_string(),
10            expected: n.to_string(),
11            got: argc,
12        }),
13        Arity::Variadic { min } if argc < *min => Err(EvalError::Arity {
14            name: name.to_string(),
15            expected: format!("{}+", min),
16            got: argc,
17        }),
18        _ => Ok(()),
19    }
20}
21
22/// Return the canonical type tag for a value (used by protocol dispatch).
23pub fn type_tag_of(val: &Value) -> Arc<str> {
24    match val {
25        Value::Nil => Arc::from("nil"),
26        Value::Bool(_) => Arc::from("Boolean"),
27        Value::Long(_) => Arc::from("Long"),
28        Value::Double(_) => Arc::from("Double"),
29        Value::BigInt(_) => Arc::from("BigInt"),
30        Value::BigDecimal(_) => Arc::from("BigDecimal"),
31        Value::Ratio(_) => Arc::from("Ratio"),
32        Value::Char(_) => Arc::from("Character"),
33        Value::Str(_) => Arc::from("String"),
34        Value::Keyword(_) => Arc::from("Keyword"),
35        Value::Symbol(_) => Arc::from("Symbol"),
36        Value::List(_) | Value::Cons(_) | Value::LazySeq(_) => Arc::from("List"),
37        Value::Vector(_) => Arc::from("Vector"),
38        Value::Map(_) => Arc::from("Map"),
39        Value::Set(_) => Arc::from("Set"),
40        Value::Fn(_) | Value::NativeFunction(_) | Value::ProtocolFn(_) | Value::MultiFn(_) => {
41            Arc::from("Fn")
42        }
43        Value::Atom(_) => Arc::from("Atom"),
44        Value::Var(_) => Arc::from("Var"),
45        Value::Protocol(_) => Arc::from("Protocol"),
46        Value::Volatile(_) => Arc::from("Volatile"),
47        Value::Delay(_) => Arc::from("Delay"),
48        Value::Promise(_) => Arc::from("Promise"),
49        Value::Future(_) => Arc::from("Future"),
50        Value::Agent(_) => Arc::from("Agent"),
51        Value::TypeInstance(ti) => ti.get().type_tag.clone(),
52        Value::NativeObject(obj) => Arc::from(obj.get().type_tag()),
53        Value::Resource(_) => Arc::from("Resource"),
54        _ => Arc::from("Object"),
55    }
56}
57
58/// Allocation-free check that `val`'s protocol dispatch tag equals `tag`.
59///
60/// Must agree exactly with [`type_tag_of`] — it exists so inline caches
61/// (`rt_call_ic` in `cljrs-compiler`'s rt_abi) can validate a cached dispatch
62/// tag on the hot path without building a fresh `Arc<str>` per call.
63pub fn type_tag_matches(val: &Value, tag: &str) -> bool {
64    match val {
65        Value::TypeInstance(ti) => &*ti.get().type_tag == tag,
66        Value::NativeObject(obj) => obj.get().type_tag() == tag,
67        _ => {
68            // All remaining variants map to a static tag; compare without
69            // allocating.  `type_tag_of` is the source of truth.
70            match val {
71                Value::Nil => "nil",
72                Value::Bool(_) => "Boolean",
73                Value::Long(_) => "Long",
74                Value::Double(_) => "Double",
75                Value::BigInt(_) => "BigInt",
76                Value::BigDecimal(_) => "BigDecimal",
77                Value::Ratio(_) => "Ratio",
78                Value::Char(_) => "Character",
79                Value::Str(_) => "String",
80                Value::Keyword(_) => "Keyword",
81                Value::Symbol(_) => "Symbol",
82                Value::List(_) | Value::Cons(_) | Value::LazySeq(_) => "List",
83                Value::Vector(_) => "Vector",
84                Value::Map(_) => "Map",
85                Value::Set(_) => "Set",
86                Value::Fn(_)
87                | Value::NativeFunction(_)
88                | Value::ProtocolFn(_)
89                | Value::MultiFn(_) => "Fn",
90                Value::Atom(_) => "Atom",
91                Value::Var(_) => "Var",
92                Value::Protocol(_) => "Protocol",
93                Value::Volatile(_) => "Volatile",
94                Value::Delay(_) => "Delay",
95                Value::Promise(_) => "Promise",
96                Value::Future(_) => "Future",
97                Value::Agent(_) => "Agent",
98                Value::Resource(_) => "Resource",
99                _ => "Object",
100            }
101        }
102        .eq(tag),
103    }
104}
105
106/// If `callee` is an `^:async` Clojure function and an async runtime is
107/// registered, spawn its body as a task and return a `Value::Future`.
108///
109/// Returns `None` when there is no async runtime or the callee is not an async
110/// function, in which case the caller proceeds with the normal synchronous
111/// call path. This is the single dispatch point shared by `apply_value` here
112/// and `eval_call` in [`crate::interp`].
113pub fn dispatch_if_async(callee: &Value, args: &[Value], env: &Env) -> Option<Value> {
114    let Value::Fn(f) = callee else { return None };
115    if !f.get().is_async {
116        return None;
117    }
118    let rt = env.globals.async_runtime()?;
119    let call_env = Env::new(env.globals.clone(), &env.current_ns);
120    Some(rt.spawn_async_call(callee.clone(), args.to_vec(), call_env))
121}
122
123/// Apply `callee` to the already-evaluated `args`.
124pub fn apply_value(callee: &Value, args: Vec<Value>, env: &mut Env) -> EvalResult {
125    // Root the callee and args so they survive any GC triggered at the safepoint.
126    // These values are on the Rust stack but not yet in any Env frame.
127    let _callee_root = crate::env::gc_roots::root_value(callee);
128    let _args_root = crate::env::gc_roots::root_values(&args);
129
130    // GC safepoint at function application boundary — blocks if collection is in progress,
131    // and initiates collection if one was requested (memory pressure).
132    crate::env::gc_roots::gc_safepoint(env);
133
134    match callee {
135        Value::NativeFunction(nf) => {
136            crate::env::policy::check_native(&nf.get().name)?;
137            check_arity(&nf.get().arity, args.len(), &nf.get().name)?;
138            // Register the caller's env as a GC root: native functions may
139            // call back into Clojure (via invoke()), which creates a fresh Env
140            // and may trigger GC.
141            let _caller_root = crate::env::gc_roots::push_env_root(env);
142            crate::env::callback::push_eval_context(env);
143            let result =
144                (nf.get().func)(&args).map_err(crate::env::error::value_error_to_eval_error);
145            crate::env::callback::pop_eval_context();
146            result
147        }
148        Value::Fn(f) => {
149            if let Some(fut) = dispatch_if_async(callee, &args, env) {
150                return Ok(fut);
151            }
152            env.call_cljrs_fn(f.get(), &args)
153        }
154        Value::BoundFn(bf) => {
155            let bf_ref = bf.get();
156            // Push captured bindings as a frame on top of the current stack.
157            // This means captured bindings take priority over the caller's,
158            // but vars not in the capture fall through to the caller's frames.
159            let _guard = crate::env::dynamics::push_frame(bf_ref.captured_bindings.clone());
160            apply_value(&bf_ref.wrapped, args, env)
161        }
162        Value::ProtocolFn(pf) => {
163            let pf_ref = pf.get();
164            let dispatch_val = args.first().ok_or_else(|| {
165                EvalError::Runtime(format!(
166                    "{}: requires at least 1 argument",
167                    pf_ref.method_name
168                ))
169            })?;
170
171            // `(defprotocol P :extend-via-metadata true ...)` — an instance
172            // implements the protocol by carrying an impl fn in its metadata,
173            // keyed by the fully-qualified symbol naming the protocol method
174            // (e.g. `` (with-meta {} {`my-method (fn [this] ...)}) ``, which
175            // syntax-quote expands to `{my.ns/my-method (fn [this] ...)}`).
176            // This mirrors real Clojure's `MethodImplCache` dispatch, which
177            // looks the method up in `(meta x)` by `(.sym cache)` — the var's
178            // qualified symbol, not the callable itself.  Metadata impls win
179            // over type-tag impls, and apply even to values (like a plain
180            // map) with no `extend-type`.
181            if pf_ref.protocol.get().extend_via_metadata
182                && let Some(Value::Map(m)) = dispatch_val.get_meta()
183            {
184                let proto = pf_ref.protocol.get();
185                let method_sym = Value::Symbol(cljrs_gc::GcPtr::new(
186                    cljrs_value::Symbol::qualified(proto.ns.clone(), pf_ref.method_name.clone()),
187                ));
188                if let Some(impl_fn) = m.get(&method_sym) {
189                    let _impl_root = crate::env::gc_roots::root_value(&impl_fn);
190                    return apply_value(&impl_fn, args, env);
191                }
192            }
193
194            let tag = type_tag_of(dispatch_val);
195            let impls = pf_ref.protocol.get().impls.lock().unwrap();
196            let impl_fn = impls
197                .get(tag.as_ref())
198                .and_then(|m| m.get(pf_ref.method_name.as_ref()))
199                .cloned()
200                .ok_or_else(|| {
201                    EvalError::Runtime(format!(
202                        "No implementation of protocol {} for type {}",
203                        pf_ref.protocol.get().name,
204                        tag
205                    ))
206                })?;
207            drop(impls);
208            let _impl_root = crate::env::gc_roots::root_value(&impl_fn);
209            apply_value(&impl_fn, args, env)
210        }
211        Value::MultiFn(mf) => {
212            let mf_ref = mf.get();
213            let dispatch_val = apply_value(&mf_ref.dispatch_fn, args.clone(), env)?;
214            let _dispatch_root = crate::env::gc_roots::root_value(&dispatch_val);
215            cljrs_gc::safepoint();
216            let key = format!("{}", dispatch_val);
217            let methods = mf_ref.methods.lock().unwrap();
218            let impl_fn = methods
219                .get(&key)
220                .or_else(|| methods.get(&mf_ref.default_dispatch))
221                .cloned()
222                .ok_or_else(|| {
223                    EvalError::Runtime(format!(
224                        "No method in multimethod '{}' for dispatch value {}",
225                        mf_ref.name, key
226                    ))
227                })?;
228            drop(methods);
229            let _impl_root = crate::env::gc_roots::root_value(&impl_fn);
230            apply_value(&impl_fn, args, env)
231        }
232        Value::Keyword(_kw) => {
233            // (kw map-or-record) → map.get(kw)
234            let default = || args.get(1).cloned().unwrap_or(Value::Nil);
235            let target = args.first().map(|a| a.unwrap_meta());
236            match target {
237                Some(Value::Map(m)) => Ok(m.get(callee).unwrap_or_else(default)),
238                Some(Value::TypeInstance(ti)) => {
239                    Ok(ti.get().fields.get(callee).unwrap_or_else(default))
240                }
241                Some(Value::Nil) => Ok(default()),
242                _ => Ok(Value::Nil),
243            }
244        }
245        Value::Map(m) => {
246            // (map key) → map.get(key)
247            match args.first() {
248                Some(k) => Ok(m
249                    .get(k)
250                    .unwrap_or(args.get(1).cloned().unwrap_or(Value::Nil))),
251                None => Ok(Value::Nil),
252            }
253        }
254        Value::Set(s) => match args.first() {
255            Some(k) => {
256                if s.contains(k) {
257                    Ok(k.clone())
258                } else {
259                    Ok(Value::Nil)
260                }
261            }
262            None => Ok(Value::Nil),
263        },
264        Value::WithMeta(inner, _) => apply_value(inner, args, env),
265        Value::Var(v) => {
266            // Vars in function position are transparently deref'd (IFn on Var).
267            // The IR interpreter uses DefVar to create per-call mutable cells for
268            // letfn / named-fn self-recursion; those cells are captured as
269            // Value::Var and called directly.
270            let inner = crate::env::dynamics::deref_var(v).ok_or_else(|| {
271                EvalError::Runtime(format!(
272                    "unbound var {}/{} used as function",
273                    v.get().namespace,
274                    v.get().name,
275                ))
276            })?;
277            apply_value(&inner, args, env)
278        }
279        other => Err(EvalError::NotCallable(format!(
280            "<{}> is not callable",
281            other.type_name()
282        ))),
283    }
284}