Skip to main content

cljrs_runtime/env/
apply.rs

1use crate::env::env::{Env, GlobalEnv};
2use crate::env::error::{EvalError, EvalResult};
3use cljrs_value::{Arity, MapValue, Value};
4use std::collections::{HashMap, HashSet};
5use std::sync::Arc;
6
7fn check_arity(arity: &Arity, argc: usize, name: &str) -> EvalResult<()> {
8    match arity {
9        Arity::Fixed(n) if argc != *n => Err(EvalError::Arity {
10            name: name.to_string(),
11            expected: n.to_string(),
12            got: argc,
13        }),
14        Arity::Variadic { min } if argc < *min => Err(EvalError::Arity {
15            name: name.to_string(),
16            expected: format!("{}+", min),
17            got: argc,
18        }),
19        _ => Ok(()),
20    }
21}
22
23#[derive(Default)]
24pub(crate) struct HierarchySnapshot {
25    pub(crate) generation: u64,
26    parents: Option<MapValue>,
27    ancestors: Option<MapValue>,
28}
29
30/// Snapshot the root hierarchy and its Var generation once per method-cache
31/// miss. Reading the generation before the root means a concurrent bind can
32/// only cache new data under an old generation, which the next lookup clears.
33pub(crate) fn global_hierarchy_snapshot(globals: &GlobalEnv) -> HierarchySnapshot {
34    let Some(var) = globals.lookup_var("clojure.core", "global-hierarchy") else {
35        return HierarchySnapshot::default();
36    };
37    let generation = var.get().binding_generation();
38    let Some(Value::Map(hierarchy)) = var.get().deref() else {
39        return HierarchySnapshot {
40            generation,
41            ..HierarchySnapshot::default()
42        };
43    };
44    let relation = |name| {
45        hierarchy
46            .get(&Value::keyword(cljrs_value::Keyword::simple(name)))
47            .and_then(|value| match value {
48                Value::Map(map) => Some(map),
49                _ => None,
50            })
51    };
52    HierarchySnapshot {
53        generation,
54        parents: relation("parents"),
55        ancestors: relation("ancestors"),
56    }
57}
58
59fn isa_with_ancestors(child: &Value, parent: &Value, ancestors: Option<&MapValue>) -> bool {
60    if child == parent {
61        return true;
62    }
63    if let Some(ancestors) = ancestors
64        && let Some(Value::Set(of_child)) = ancestors.get(child)
65        && of_child.contains(parent)
66    {
67        return true;
68    }
69    if let (Value::Vector(c), Value::Vector(p)) = (child, parent) {
70        let (c, p) = (c.get(), p.get());
71        return c.count() == p.count()
72            && (0..c.count()).all(|i| match (c.nth(i), p.nth(i)) {
73                (Some(cv), Some(pv)) => isa_with_ancestors(cv, pv, ancestors),
74                _ => false,
75            });
76    }
77    false
78}
79
80fn prefers_with_table(
81    prefers: &HashMap<String, Vec<String>>,
82    parents: Option<&MapValue>,
83    x: &Value,
84    y: &Value,
85) -> bool {
86    fn recur(
87        prefers: &HashMap<String, Vec<String>>,
88        parents: Option<&MapValue>,
89        x: &Value,
90        y: &Value,
91        seen: &mut HashSet<(String, String)>,
92    ) -> bool {
93        let x_key = format!("{x}");
94        let y_key = format!("{y}");
95        if !seen.insert((x_key.clone(), y_key.clone())) {
96            return false;
97        }
98        if prefers
99            .get(&x_key)
100            .is_some_and(|over| over.contains(&y_key))
101        {
102            return true;
103        }
104        let Some(parents) = parents else {
105            return false;
106        };
107        if let Some(Value::Set(y_parents)) = parents.get(y)
108            && y_parents
109                .iter()
110                .any(|parent| recur(prefers, Some(parents), x, parent, seen))
111        {
112            return true;
113        }
114        if let Some(Value::Set(x_parents)) = parents.get(x)
115            && x_parents
116                .iter()
117                .any(|parent| recur(prefers, Some(parents), parent, y, seen))
118        {
119            return true;
120        }
121        false
122    }
123
124    recur(prefers, parents, x, y, &mut HashSet::new())
125}
126
127/// Clojure-compatible preference lookup, including preferences inherited
128/// through the parents of either dispatch value.
129pub(crate) fn prefers_in_hierarchy(
130    mf: &cljrs_value::MultiFn,
131    x: &Value,
132    y: &Value,
133    hierarchy: &HierarchySnapshot,
134) -> bool {
135    let prefers = mf.prefers.lock().unwrap();
136    prefers_with_table(&prefers, hierarchy.parents.as_ref(), x, y)
137}
138
139fn join_conflicts(matches: &[&(String, Value)]) -> String {
140    match matches {
141        [] => String::new(),
142        [only] => only.0.clone(),
143        [first, second] => format!("{} and {}", first.0, second.0),
144        many => {
145            let (last, initial) = many.split_last().unwrap();
146            format!(
147                "{}, and {}",
148                initial
149                    .iter()
150                    .map(|entry| entry.0.as_str())
151                    .collect::<Vec<_>>()
152                    .join(", "),
153                last.0
154            )
155        }
156    }
157}
158
159/// The method key a dispatch value inherits, or `None` when no registered
160/// dispatch value is an ancestor of it.
161///
162/// Errors when several unrelated methods match and no `prefer-method` call
163/// separates them — the same ambiguity Clojure reports.
164fn hierarchy_method_key(
165    mf: &cljrs_value::MultiFn,
166    dispatch_val: &Value,
167    hierarchy: &HierarchySnapshot,
168) -> EvalResult<Option<String>> {
169    let dispatch_vals = mf.dispatch_vals.lock().unwrap();
170    let mut matches: Vec<(String, Value)> = dispatch_vals
171        .iter()
172        .filter(|(k, v)| {
173            k.as_str() != mf.default_dispatch
174                && isa_with_ancestors(dispatch_val, v, hierarchy.ancestors.as_ref())
175        })
176        .map(|(k, v)| (k.clone(), v.clone()))
177        .collect();
178    drop(dispatch_vals);
179    matches.sort_by(|a, b| a.0.cmp(&b.0));
180    if matches.len() <= 1 {
181        return Ok(matches.into_iter().next().map(|(k, _)| k));
182    }
183
184    let prefers = mf.prefers.lock().unwrap();
185    let dominates = |a: &(String, Value), b: &(String, Value)| {
186        prefers_with_table(&prefers, hierarchy.parents.as_ref(), &a.1, &b.1)
187            || isa_with_ancestors(&a.1, &b.1, hierarchy.ancestors.as_ref())
188    };
189    let best: Vec<&(String, Value)> = matches
190        .iter()
191        .filter(|a| {
192            !matches
193                .iter()
194                .any(|b| a.0 != b.0 && dominates(b, a) && !dominates(a, b))
195        })
196        .collect();
197    match best.as_slice() {
198        [only] => Ok(Some(only.0.clone())),
199        _ => Err(EvalError::Runtime(format!(
200            "Multiple methods in multimethod '{}' match dispatch value {}: {}, and {} is preferred",
201            mf.name,
202            dispatch_val,
203            join_conflicts(&best),
204            if best.len() == 2 { "neither" } else { "none" }
205        ))),
206    }
207}
208
209/// Return the canonical type tag for a value (used by protocol dispatch).
210pub fn type_tag_of(val: &Value) -> Arc<str> {
211    match val.unwrap_meta() {
212        Value::Nil => Arc::from("nil"),
213        Value::Bool(_) => Arc::from("Boolean"),
214        Value::Long(_) => Arc::from("Long"),
215        Value::Double(_) => Arc::from("Double"),
216        Value::BigInt(_) => Arc::from("BigInt"),
217        Value::BigDecimal(_) => Arc::from("BigDecimal"),
218        Value::Ratio(_) => Arc::from("Ratio"),
219        Value::Char(_) => Arc::from("Character"),
220        Value::Str(_) => Arc::from("String"),
221        Value::Keyword(_) => Arc::from("Keyword"),
222        Value::Symbol(_) => Arc::from("Symbol"),
223        Value::List(_) | Value::Cons(_) | Value::LazySeq(_) => Arc::from("List"),
224        Value::Vector(_) => Arc::from("Vector"),
225        Value::Map(_) => Arc::from("Map"),
226        Value::Set(_) => Arc::from("Set"),
227        Value::Fn(_) | Value::NativeFunction(_) | Value::ProtocolFn(_) | Value::MultiFn(_) => {
228            Arc::from("Fn")
229        }
230        Value::Atom(_) => Arc::from("Atom"),
231        Value::Var(_) => Arc::from("Var"),
232        Value::Protocol(_) => Arc::from("Protocol"),
233        Value::Volatile(_) => Arc::from("Volatile"),
234        Value::Delay(_) => Arc::from("Delay"),
235        Value::Promise(_) => Arc::from("Promise"),
236        Value::Future(_) => Arc::from("Future"),
237        Value::Agent(_) => Arc::from("Agent"),
238        Value::TypeInstance(ti) => ti.get().type_tag.clone(),
239        Value::NativeObject(obj) => Arc::from(obj.get().type_tag()),
240        Value::Resource(_) => Arc::from("Resource"),
241        _ => Arc::from("Object"),
242    }
243}
244
245/// Allocation-free check that `val`'s protocol dispatch tag equals `tag`.
246///
247/// Must agree exactly with [`type_tag_of`] — it exists so inline caches
248/// (`rt_call_ic` in `cljrs-compiler`'s rt_abi) can validate a cached dispatch
249/// tag on the hot path without building a fresh `Arc<str>` per call.
250pub fn type_tag_matches(val: &Value, tag: &str) -> bool {
251    // `type_tag_of` unwraps a metadata wrapper; so must this, or an annotated
252    // dispatch value is a permanent inline-cache miss that re-resolves and
253    // rewrites the entry under lock on every call.
254    let val = val.unwrap_meta();
255    match val {
256        Value::TypeInstance(ti) => &*ti.get().type_tag == tag,
257        Value::NativeObject(obj) => obj.get().type_tag() == tag,
258        _ => {
259            // All remaining variants map to a static tag; compare without
260            // allocating.  `type_tag_of` is the source of truth.
261            match val {
262                Value::Nil => "nil",
263                Value::Bool(_) => "Boolean",
264                Value::Long(_) => "Long",
265                Value::Double(_) => "Double",
266                Value::BigInt(_) => "BigInt",
267                Value::BigDecimal(_) => "BigDecimal",
268                Value::Ratio(_) => "Ratio",
269                Value::Char(_) => "Character",
270                Value::Str(_) => "String",
271                Value::Keyword(_) => "Keyword",
272                Value::Symbol(_) => "Symbol",
273                Value::List(_) | Value::Cons(_) | Value::LazySeq(_) => "List",
274                Value::Vector(_) => "Vector",
275                Value::Map(_) => "Map",
276                Value::Set(_) => "Set",
277                Value::Fn(_)
278                | Value::NativeFunction(_)
279                | Value::ProtocolFn(_)
280                | Value::MultiFn(_) => "Fn",
281                Value::Atom(_) => "Atom",
282                Value::Var(_) => "Var",
283                Value::Protocol(_) => "Protocol",
284                Value::Volatile(_) => "Volatile",
285                Value::Delay(_) => "Delay",
286                Value::Promise(_) => "Promise",
287                Value::Future(_) => "Future",
288                Value::Agent(_) => "Agent",
289                Value::Resource(_) => "Resource",
290                _ => "Object",
291            }
292        }
293        .eq(tag),
294    }
295}
296
297/// If `callee` is an `^:async` Clojure function and an async runtime is
298/// registered, spawn its body as a task and return a `Value::Future`.
299///
300/// Returns `None` when there is no async runtime or the callee is not an async
301/// function, in which case the caller proceeds with the normal synchronous
302/// call path. This is the single dispatch point shared by `apply_value` here
303/// and `eval_call` in [`crate::interp`].
304pub fn dispatch_if_async(callee: &Value, args: &[Value], env: &Env) -> Option<Value> {
305    let Value::Fn(f) = callee else { return None };
306    if !f.get().is_async {
307        return None;
308    }
309    let rt = env.globals.async_runtime()?;
310    let call_env = Env::new(env.globals.clone(), &env.current_ns);
311    Some(rt.spawn_async_call(callee.clone(), args.to_vec(), call_env))
312}
313
314/// Apply `callee` to the already-evaluated `args`.
315pub fn apply_value(callee: &Value, args: Vec<Value>, env: &mut Env) -> EvalResult {
316    // Root the callee and args so they survive any GC triggered at the safepoint.
317    // These values are on the Rust stack but not yet in any Env frame.
318    let _callee_root = crate::env::gc_roots::root_value(callee);
319    let _args_root = crate::env::gc_roots::root_values(&args);
320
321    // GC safepoint at function application boundary — blocks if collection is in progress,
322    // and initiates collection if one was requested (memory pressure).
323    crate::env::gc_roots::gc_safepoint(env);
324
325    match callee {
326        Value::NativeFunction(nf) => {
327            crate::env::policy::check_native(&nf.get().name)?;
328            check_arity(&nf.get().arity, args.len(), &nf.get().name)?;
329            // Register the caller's env as a GC root: native functions may
330            // call back into Clojure (via invoke()), which creates a fresh Env
331            // and may trigger GC.
332            let _caller_root = crate::env::gc_roots::push_env_root(env);
333            crate::env::callback::push_eval_context(env);
334            let result =
335                (nf.get().func)(&args).map_err(crate::env::error::value_error_to_eval_error);
336            crate::env::callback::pop_eval_context();
337            result
338        }
339        Value::Fn(f) => {
340            if let Some(fut) = dispatch_if_async(callee, &args, env) {
341                return Ok(fut);
342            }
343            env.call_cljrs_fn(f.get(), &args)
344        }
345        Value::BoundFn(bf) => {
346            let bf_ref = bf.get();
347            // Push captured bindings as a frame on top of the current stack.
348            // This means captured bindings take priority over the caller's,
349            // but vars not in the capture fall through to the caller's frames.
350            let _guard = crate::env::dynamics::push_frame(bf_ref.captured_bindings.clone());
351            apply_value(&bf_ref.wrapped, args, env)
352        }
353        Value::ProtocolFn(pf) => {
354            let pf_ref = pf.get();
355            let dispatch_val = args.first().ok_or_else(|| {
356                EvalError::Runtime(format!(
357                    "{}: requires at least 1 argument",
358                    pf_ref.method_name
359                ))
360            })?;
361
362            // `(defprotocol P :extend-via-metadata true ...)` — an instance
363            // implements the protocol by carrying an impl fn in its metadata,
364            // keyed by the fully-qualified symbol naming the protocol method
365            // (e.g. `` (with-meta {} {`my-method (fn [this] ...)}) ``, which
366            // syntax-quote expands to `{my.ns/my-method (fn [this] ...)}`).
367            // This mirrors real Clojure's `MethodImplCache` dispatch, which
368            // looks the method up in `(meta x)` by `(.sym cache)` — the var's
369            // qualified symbol, not the callable itself.  Metadata impls win
370            // over type-tag impls, and apply even to values (like a plain
371            // map) with no `extend-type`.
372            if pf_ref.protocol.get().extend_via_metadata
373                && let Some(Value::Map(m)) = dispatch_val.get_meta()
374            {
375                let proto = pf_ref.protocol.get();
376                let method_sym = Value::Symbol(cljrs_gc::GcPtr::new(
377                    cljrs_value::Symbol::qualified(proto.ns.clone(), pf_ref.method_name.clone()),
378                ));
379                if let Some(impl_fn) = m.get(&method_sym) {
380                    let _impl_root = crate::env::gc_roots::root_value(&impl_fn);
381                    return apply_value(&impl_fn, args, env);
382                }
383            }
384
385            let tag = type_tag_of(dispatch_val);
386            let impls = pf_ref.protocol.get().impls.lock().unwrap();
387            let impl_fn = impls
388                .get(tag.as_ref())
389                .and_then(|m| m.get(pf_ref.method_name.as_ref()))
390                .cloned()
391                .ok_or_else(|| {
392                    EvalError::Runtime(format!(
393                        "No implementation of protocol {} for type {}",
394                        pf_ref.protocol.get().name,
395                        tag
396                    ))
397                })?;
398            drop(impls);
399            let _impl_root = crate::env::gc_roots::root_value(&impl_fn);
400            apply_value(&impl_fn, args, env)
401        }
402        Value::MultiFn(mf) => {
403            let mf_ref = mf.get();
404            let dispatch_val = apply_value(&mf_ref.dispatch_fn, args.clone(), env)?;
405            let _dispatch_root = crate::env::gc_roots::root_value(&dispatch_val);
406            cljrs_gc::safepoint();
407            let key = format!("{}", dispatch_val);
408            let exact = mf_ref.methods.lock().unwrap().get(&key).cloned();
409            let impl_fn = match exact {
410                Some(f) => f,
411                None => {
412                    let hierarchy = global_hierarchy_snapshot(&env.globals);
413                    let method_generation = mf_ref.method_generation();
414                    let cached =
415                        mf_ref.cached_method(&key, hierarchy.generation, method_generation);
416                    let method_key = match cached {
417                        Some(cached) => cached,
418                        None => {
419                            let inherited =
420                                hierarchy_method_key(mf_ref, &dispatch_val, &hierarchy)?;
421                            let resolved = match inherited {
422                                Some(k) => k,
423                                None if mf_ref
424                                    .methods
425                                    .lock()
426                                    .unwrap()
427                                    .contains_key(&mf_ref.default_dispatch) =>
428                                {
429                                    mf_ref.default_dispatch.clone()
430                                }
431                                None => {
432                                    return Err(EvalError::Runtime(format!(
433                                        "No method in multimethod '{}' for dispatch value {}",
434                                        mf_ref.name, key
435                                    )));
436                                }
437                            };
438                            mf_ref.cache_method(
439                                key.clone(),
440                                resolved.clone(),
441                                hierarchy.generation,
442                                method_generation,
443                            );
444                            resolved
445                        }
446                    };
447                    mf_ref
448                        .methods
449                        .lock()
450                        .unwrap()
451                        .get(&method_key)
452                        .cloned()
453                        .ok_or_else(|| {
454                            EvalError::Runtime(format!(
455                                "No method in multimethod '{}' for dispatch value {}",
456                                mf_ref.name, key
457                            ))
458                        })?
459                }
460            };
461            let _impl_root = crate::env::gc_roots::root_value(&impl_fn);
462            apply_value(&impl_fn, args, env)
463        }
464        Value::Keyword(_kw) => {
465            // (kw map-or-record) → map.get(kw)
466            let default = || args.get(1).cloned().unwrap_or(Value::Nil);
467            let target = args.first().map(|a| a.unwrap_meta());
468            match target {
469                Some(Value::Map(m)) => Ok(m.get(callee).unwrap_or_else(default)),
470                Some(Value::TypeInstance(ti)) => {
471                    Ok(ti.get().fields.get(callee).unwrap_or_else(default))
472                }
473                Some(Value::Nil) => Ok(default()),
474                _ => Ok(Value::Nil),
475            }
476        }
477        Value::Map(m) => {
478            // (map key) → map.get(key)
479            match args.first() {
480                Some(k) => Ok(m
481                    .get(k)
482                    .unwrap_or(args.get(1).cloned().unwrap_or(Value::Nil))),
483                None => Ok(Value::Nil),
484            }
485        }
486        Value::Set(s) => match args.first() {
487            Some(k) => {
488                if s.contains(k) {
489                    Ok(k.clone())
490                } else {
491                    Ok(Value::Nil)
492                }
493            }
494            None => Ok(Value::Nil),
495        },
496        Value::WithMeta(inner, _) => apply_value(inner, args, env),
497        Value::Var(v) => {
498            // Vars in function position are transparently deref'd (IFn on Var).
499            // The IR interpreter uses DefVar to create per-call mutable cells for
500            // letfn / named-fn self-recursion; those cells are captured as
501            // Value::Var and called directly.
502            let inner = crate::env::dynamics::deref_var(v).ok_or_else(|| {
503                EvalError::Runtime(format!(
504                    "unbound var {}/{} used as function",
505                    v.get().namespace,
506                    v.get().name,
507                ))
508            })?;
509            apply_value(&inner, args, env)
510        }
511        other => Err(EvalError::NotCallable(format!(
512            "<{}> is not callable",
513            other.type_name()
514        ))),
515    }
516}