Skip to main content

cljrs_runtime/interp/
apply.rs

1//! Function application and the recur trampoline.
2
3use crate::builtins::form::form_to_value;
4use cljrs_gc::GcPtr;
5use cljrs_reader::{Form, FormKind};
6use cljrs_value::{
7    Atom, CljxFn, CljxFnArity, Delay, LazySeq, MapValue, PersistentList, Symbol, Thunk, Value,
8    Volatile,
9};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use crate::env::env::Env;
14use crate::env::error::{EvalError, EvalResult};
15use crate::interp::destructure::value_to_seq_vec;
16use crate::interp::eval::eval;
17
18/// Convert an EvalError to a Value for storage (e.g. agent errors).
19/// Preserves Thrown values (ex-info); other errors become strings.
20#[allow(dead_code)]
21fn eval_error_to_value(e: EvalError) -> Value {
22    match e {
23        EvalError::Thrown(v) => v,
24        other => Value::string(format!("{other}")),
25    }
26}
27
28// ── Watch notification ───────────────────────────────────────────────────────
29
30/// Fire all watches on a watchable (atom, var, agent).
31/// Each watch fn is called as `(f key ref old new)`.
32/// Errors thrown by watch fns are re-thrown (matching Clojure behavior).
33fn fire_watches(
34    watches: &std::sync::Mutex<Vec<(Value, Value)>>,
35    reference: &Value,
36    old: &Value,
37    new: &Value,
38    env: &mut Env,
39) {
40    let ws: Vec<(Value, Value)> = watches.lock().unwrap().clone();
41    for (key, f) in &ws {
42        let args = vec![key.clone(), reference.clone(), old.clone(), new.clone()];
43        if let Err(e) = crate::env::apply::apply_value(f, args, env) {
44            // Re-throw watch errors (Clojure behavior: exception propagates to caller)
45            // We can't return EvalResult from here, so we store and re-throw below.
46            // For now, propagate by re-invoking so it surfaces.
47            // Actually, in Clojure, watch exceptions propagate to the mutating call.
48            // We need to handle this differently — but for simplicity, just ignore for now
49            // and let the caller check. Actually let's just use a thread-local to propagate.
50            WATCH_ERROR.with(|cell| {
51                cell.borrow_mut().replace(e);
52            });
53            return;
54        }
55    }
56}
57
58thread_local! {
59    static WATCH_ERROR: std::cell::RefCell<Option<EvalError>> = const { std::cell::RefCell::new(None) };
60}
61
62/// Check if a watch error occurred and propagate it.
63fn check_watch_error() -> EvalResult<()> {
64    WATCH_ERROR.with(|cell| {
65        if let Some(e) = cell.borrow_mut().take() {
66            Err(e)
67        } else {
68            Ok(())
69        }
70    })
71}
72
73// ── ClosureThunk ──────────────────────────────────────────────────────────────
74
75/// A Thunk that calls a zero-arg Clojure closure when forced.
76#[derive(Debug)]
77pub struct ClosureThunk {
78    pub f: CljxFn,
79    pub globals: std::sync::Arc<crate::env::env::GlobalEnv>,
80    pub ns: std::sync::Arc<str>,
81}
82
83/// A Thunk that wraps any zero-arg callable `Value` (a `Value::Fn`, a
84/// `Value::NativeFunction`, etc.) and forces by routing through
85/// `apply_value`.  Used by [`make_lazy_seq_from_fn`] when the supplied
86/// value isn't a plain Clojure fn — for example the IR interpreter's
87/// `AllocClosure` produces a `NativeFunction` wrapping the IR closure.
88struct CallableValueThunk {
89    callee: Value,
90    globals: std::sync::Arc<crate::env::env::GlobalEnv>,
91    ns: std::sync::Arc<str>,
92}
93
94impl std::fmt::Debug for CallableValueThunk {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.debug_struct("CallableValueThunk")
97            .field("callee", &self.callee.type_name())
98            .field("ns", &self.ns)
99            .finish()
100    }
101}
102
103impl cljrs_gc::Trace for CallableValueThunk {
104    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
105        self.callee.trace(visitor);
106    }
107}
108
109impl Thunk for CallableValueThunk {
110    fn force(&self) -> Result<Value, String> {
111        let mut env = Env::new(self.globals.clone(), &self.ns);
112        crate::env::apply::apply_value(&self.callee, Vec::new(), &mut env)
113            .map_err(|e| format!("{e}"))
114    }
115}
116
117/// Wrap a zero-arg callable value in a `Value::LazySeq` whose `force`
118/// calls it.
119///
120/// Accepts any `Value` whose type-name is "fn" — `Value::Fn`,
121/// `Value::NativeFunction`, `Value::BoundFn`, etc. — and unwraps any
122/// surrounding `WithMeta`.  The fast path stays in `Value::Fn` (a direct
123/// `ClosureThunk`); other callables route through a thunk that dispatches
124/// via `apply_value` on force.
125///
126/// This is the value-level analogue of [`handle_make_lazy_seq`], usable
127/// from contexts that already have a `Value` (e.g. the IR interpreter)
128/// rather than a `Form`.
129pub fn make_lazy_seq_from_fn(
130    f_val: &Value,
131    globals: std::sync::Arc<crate::env::env::GlobalEnv>,
132    ns: std::sync::Arc<str>,
133) -> EvalResult {
134    let unwrapped = f_val.unwrap_meta();
135    if let Value::Fn(g) = unwrapped {
136        let thunk = ClosureThunk {
137            f: g.get().clone(),
138            globals,
139            ns,
140        };
141        return Ok(Value::LazySeq(GcPtr::new(LazySeq::new(Box::new(thunk)))));
142    }
143    // Anything else with type-name "fn" is acceptable; route through
144    // apply_value at force-time.  Reject non-callable values up front.
145    if unwrapped.type_name() != "fn" {
146        return Err(EvalError::Runtime(format!(
147            "make-lazy-seq requires a fn, got {}",
148            unwrapped.type_name(),
149        )));
150    }
151    let thunk = CallableValueThunk {
152        callee: unwrapped.clone(),
153        globals,
154        ns,
155    };
156    Ok(Value::LazySeq(GcPtr::new(LazySeq::new(Box::new(thunk)))))
157}
158
159impl cljrs_gc::Trace for ClosureThunk {
160    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
161        self.f.trace(visitor);
162    }
163}
164
165impl Thunk for ClosureThunk {
166    fn force(&self) -> Result<Value, String> {
167        // Root the closed-over values so they survive GC.  The thunk may live
168        // on the Rust stack outside any Env frame (e.g., after LazySeq::realize
169        // drops its Mutex guard), so GC wouldn't trace them otherwise.
170        let _closed_root = crate::env::gc_roots::root_values(&self.f.closed_over_vals);
171        let mut env = Env::with_closure(self.globals.clone(), &self.ns, &self.f);
172        call_cljrs_fn(&self.f, &[], &mut env).map_err(|e| format!("{e}"))
173    }
174}
175
176/// Evaluate a call expression `(func-form arg1 arg2 ...)`.
177///
178/// Handles:
179/// - Macro expansion (if callee is a macro).
180/// - The `apply` function (spread last arg).
181/// - The `swap!` function (needs env to call the function).
182/// - Regular function calls.
183pub fn eval_call(func_form: &Form, arg_forms: &[Form], env: &mut Env) -> EvalResult {
184    // Interop: (.methodName target args...) — method call syntax.
185    if let FormKind::Symbol(s) = &func_form.kind
186        && let Some(method) = s.strip_prefix('.')
187        && !method.is_empty()
188        && method != "."
189    {
190        return eval_method_call(method, arg_forms, env);
191    }
192
193    // Evaluate the callee first.
194    let callee = eval(func_form, env)?;
195
196    // Root the callee so it survives any GC triggered during argument evaluation.
197    let _callee_root = crate::env::gc_roots::root_value(&callee);
198
199    // Macro check: expand then re-eval.
200    if let Value::Macro(mfn) = &callee {
201        let expanded = macro_apply(mfn.get(), func_form, arg_forms, env)?;
202        return eval(&expanded, env);
203    }
204
205    // Special case: `apply` native fn — spread last arg.
206    if let Value::NativeFunction(nf) = &callee {
207        crate::env::policy::check_native(&nf.get().name)?;
208        match nf.get().name.as_ref() {
209            "apply" => return handle_apply_call(arg_forms, env),
210            "atom" => return handle_atom_call(arg_forms, env),
211            "reset!" => return handle_reset_bang(arg_forms, env),
212            "swap!" => return handle_swap_call(arg_forms, env),
213            "volatile!" => return handle_volatile(arg_forms, env),
214            "vreset!" => return handle_vreset(arg_forms, env),
215            "agent" => return handle_agent_call(arg_forms, env),
216            "make-lazy-seq" => return handle_make_lazy_seq(arg_forms, env),
217            "make-delay" => return handle_make_delay(arg_forms, env),
218            "vswap!" => return handle_vswap(arg_forms, env),
219            "send" | "send-off" => return handle_send(arg_forms, env),
220            "with-bindings*" => return handle_with_bindings(arg_forms, env),
221            "alter-var-root" => return handle_alter_var_root(arg_forms, env),
222            "vary-meta" => return handle_vary_meta(arg_forms, env),
223            "find-ns" | "the-ns" => return handle_find_ns(arg_forms, env),
224            "ns-interns" | "ns-publics" => return handle_ns_interns(arg_forms, env),
225            "ns-refers" => return handle_ns_refers(arg_forms, env),
226            "ns-map" => return handle_ns_map(arg_forms, env),
227            "all-ns" => return handle_all_ns(arg_forms, env),
228            "create-ns" => return handle_create_ns(arg_forms, env),
229            "ns-aliases" => return handle_ns_aliases(arg_forms, env),
230            "remove-ns" => return handle_remove_ns(arg_forms, env),
231            "alter-meta!" => return handle_alter_meta(arg_forms, env),
232            "ns-resolve" => return handle_ns_resolve(arg_forms, env),
233            "resolve" => return handle_resolve(arg_forms, env),
234            "intern" => return handle_intern(arg_forms, env),
235            "bound-fn*" => return handle_bound_fn_star(arg_forms, env),
236            _ => {}
237        }
238    }
239
240    // Evaluate arguments one-at-a-time, rooting partial results so that
241    // previously-evaluated args survive any GC triggered during later evals.
242    let mut args: Vec<Value> = Vec::with_capacity(arg_forms.len());
243    for f in arg_forms {
244        // Root the already-evaluated args before each eval that could trigger GC.
245        let _args_root = crate::env::gc_roots::root_values(&args);
246        args.push(eval(f, env)?);
247    }
248
249    // For Clojure functions, dispatch through the global hook so the IR
250    // interpreter (when active) gets a chance to run cached IR instead of
251    // always falling back to tree-walking.  `globals.call_cljrs_fn` is set
252    // to `crate::tiered::apply::call_cljrs_fn` at startup when the IR tier is
253    // available, and to the plain tree-walking version otherwise.
254    //
255    // Extract the function pointer before the call so the borrow checker sees
256    // only one mutable borrow of `env` at the call site.
257    if let Value::Fn(f) = &callee {
258        // `^:async` functions dispatch through the async runtime (when one is
259        // registered), spawning the body and returning a Future immediately.
260        if let Some(fut) = crate::env::apply::dispatch_if_async(&callee, &args, env) {
261            return Ok(fut);
262        }
263        let _args_root = crate::env::gc_roots::root_values(&args);
264        crate::env::gc_roots::gc_safepoint(env);
265        let call_fn = env.globals.call_cljrs_fn;
266        return call_fn(f.get(), &args, env);
267    }
268
269    crate::env::apply::apply_value(&callee, args, env)
270}
271
272// ── Interop method calls ─────────────────────────────────────────────────────
273
274/// Evaluate `(.methodName target args...)` interop syntax.
275///
276/// Currently supports a small set of methods on built-in types:
277/// - `.indexOf` on strings and vectors
278/// - `.startsWith`, `.endsWith`, `.contains`, `.substring`, `.length`,
279///   `.charAt`, `.toUpperCase`, `.toLowerCase`, `.trim`, `.replace`,
280///   `.split` on strings
281fn eval_method_call(method: &str, arg_forms: &[Form], env: &mut Env) -> EvalResult {
282    if arg_forms.is_empty() {
283        return Err(EvalError::Runtime(format!(
284            ".{method} requires a target object"
285        )));
286    }
287    let target = eval(&arg_forms[0], env)?;
288    let args: Vec<Value> = arg_forms[1..]
289        .iter()
290        .map(|f| eval(f, env))
291        .collect::<EvalResult<_>>()?;
292
293    dispatch_method(method, &target, &args)
294}
295
296/// Dispatch `(.method target args…)` on an already-evaluated target.
297///
298/// Form-free, so the Tier-1 IR interpreter can route dot-marked
299/// `CallDirect` instructions here (see `dispatch_sentinel_by_name` in
300/// cljrs-eval) and behave exactly like the tree-walker's interop path.
301pub fn dispatch_method(method: &str, target: &Value, args: &[Value]) -> EvalResult {
302    match target {
303        Value::Str(s) => dispatch_string_method(method, s.get(), args),
304        Value::Vector(v) => dispatch_vector_method(method, v, args),
305        Value::List(_) | Value::Cons(_) | Value::LazySeq(_) => {
306            dispatch_seq_method(method, target, args)
307        }
308        _ => Err(EvalError::Runtime(format!(
309            ".{method} not supported on type {}",
310            target.type_name()
311        ))),
312    }
313}
314
315fn dispatch_string_method(method: &str, s: &str, args: &[Value]) -> EvalResult {
316    match method {
317        "indexOf" => {
318            let needle = match args.first() {
319                Some(Value::Str(s)) => s.get().to_string(),
320                Some(Value::Char(c)) => c.to_string(),
321                Some(v) => {
322                    return Err(EvalError::Runtime(format!(
323                        ".indexOf expects string or char argument, got {}",
324                        v.type_name()
325                    )));
326                }
327                None => return Err(EvalError::Runtime(".indexOf requires an argument".into())),
328            };
329            match s.find(&needle) {
330                Some(pos) => Ok(Value::Long(pos as i64)),
331                None => Ok(Value::Long(-1)),
332            }
333        }
334        "lastIndexOf" => {
335            let needle = match args.first() {
336                Some(Value::Str(s)) => s.get().to_string(),
337                Some(Value::Char(c)) => c.to_string(),
338                _ => {
339                    return Err(EvalError::Runtime(
340                        ".lastIndexOf requires a string or char argument".into(),
341                    ));
342                }
343            };
344            match s.rfind(&needle) {
345                Some(pos) => Ok(Value::Long(pos as i64)),
346                None => Ok(Value::Long(-1)),
347            }
348        }
349        "startsWith" => {
350            let prefix = require_str_arg(args, ".startsWith")?;
351            Ok(Value::Bool(s.starts_with(&prefix)))
352        }
353        "endsWith" => {
354            let suffix = require_str_arg(args, ".endsWith")?;
355            Ok(Value::Bool(s.ends_with(&suffix)))
356        }
357        "contains" => {
358            let sub = require_str_arg(args, ".contains")?;
359            Ok(Value::Bool(s.contains(&sub)))
360        }
361        "length" => Ok(Value::Long(s.len() as i64)),
362        "isEmpty" => Ok(Value::Bool(s.is_empty())),
363        "charAt" => {
364            let idx = require_long_arg(args, ".charAt")? as usize;
365            s.chars()
366                .nth(idx)
367                .map(Value::Char)
368                .ok_or_else(|| EvalError::Runtime(format!(".charAt index {idx} out of bounds")))
369        }
370        "substring" => {
371            let start = require_long_arg(args, ".substring")? as usize;
372            let end = args
373                .get(1)
374                .map(|v| match v {
375                    Value::Long(n) => Ok(*n as usize),
376                    _ => Err(EvalError::Runtime(
377                        ".substring end must be an integer".into(),
378                    )),
379                })
380                .transpose()?;
381            let result = match end {
382                Some(e) => &s[start..e.min(s.len())],
383                None => &s[start..],
384            };
385            Ok(Value::Str(GcPtr::new(result.to_string())))
386        }
387        "toUpperCase" => Ok(Value::Str(GcPtr::new(s.to_uppercase()))),
388        "toLowerCase" => Ok(Value::Str(GcPtr::new(s.to_lowercase()))),
389        "trim" => Ok(Value::Str(GcPtr::new(s.trim().to_string()))),
390        "replace" => {
391            let from = require_str_arg(args, ".replace")?;
392            let to = match args.get(1) {
393                Some(Value::Str(s)) => s.get().to_string(),
394                Some(Value::Char(c)) => c.to_string(),
395                _ => {
396                    return Err(EvalError::Runtime(
397                        ".replace requires two string arguments".into(),
398                    ));
399                }
400            };
401            Ok(Value::Str(GcPtr::new(s.replace(&from, &to))))
402        }
403        "split" => {
404            let sep = require_str_arg(args, ".split")?;
405            let parts: Vec<Value> = s
406                .split(&sep)
407                .map(|p| Value::Str(GcPtr::new(p.to_string())))
408                .collect();
409            Ok(Value::Vector(GcPtr::new(
410                cljrs_value::PersistentVector::from_iter(parts),
411            )))
412        }
413        _ => Err(EvalError::Runtime(format!(
414            ".{method} not supported on String"
415        ))),
416    }
417}
418
419fn dispatch_vector_method(
420    method: &str,
421    v: &GcPtr<cljrs_value::PersistentVector>,
422    args: &[Value],
423) -> EvalResult {
424    match method {
425        "indexOf" => {
426            let needle = args
427                .first()
428                .ok_or_else(|| EvalError::Runtime(".indexOf requires an argument".into()))?;
429            for (i, item) in v.get().iter().enumerate() {
430                if item == needle {
431                    return Ok(Value::Long(i as i64));
432                }
433            }
434            Ok(Value::Long(-1))
435        }
436        "size" | "count" => Ok(Value::Long(v.get().count() as i64)),
437        _ => Err(EvalError::Runtime(format!(
438            ".{method} not supported on Vector"
439        ))),
440    }
441}
442
443fn dispatch_seq_method(method: &str, target: &Value, args: &[Value]) -> EvalResult {
444    match method {
445        "indexOf" => {
446            let needle = args
447                .first()
448                .ok_or_else(|| EvalError::Runtime(".indexOf requires an argument".into()))?;
449            let items = crate::interp::destructure::value_to_seq_vec(target);
450            for (i, item) in items.iter().enumerate() {
451                if item == needle {
452                    return Ok(Value::Long(i as i64));
453                }
454            }
455            Ok(Value::Long(-1))
456        }
457        _ => Err(EvalError::Runtime(format!(
458            ".{method} not supported on {}",
459            target.type_name()
460        ))),
461    }
462}
463
464fn require_str_arg(args: &[Value], method: &str) -> Result<String, EvalError> {
465    match args.first() {
466        Some(Value::Str(s)) => Ok(s.get().to_string()),
467        Some(Value::Char(c)) => Ok(c.to_string()),
468        _ => Err(EvalError::Runtime(format!(
469            "{method} requires a string argument"
470        ))),
471    }
472}
473
474fn require_long_arg(args: &[Value], method: &str) -> Result<i64, EvalError> {
475    match args.first() {
476        Some(Value::Long(n)) => Ok(*n),
477        _ => Err(EvalError::Runtime(format!(
478            "{method} requires an integer argument"
479        ))),
480    }
481}
482
483/// Resolve a type symbol from `extend-type` to a canonical tag.
484/// Canonical tags ARE the short names, so this just passes through.
485pub fn resolve_type_tag(sym: &str) -> Arc<str> {
486    Arc::from(sym)
487}
488
489/// Tree-walking execution path (original implementation).
490pub fn call_cljrs_fn(f: &CljxFn, args: &[Value], caller_env: &mut Env) -> EvalResult {
491    let arity = select_arity(f, args.len())?;
492
493    // Register the caller's env as a GC root so its local bindings survive
494    // any collection triggered while we're executing the callee's body.
495    let _caller_root = crate::env::gc_roots::push_env_root(caller_env);
496
497    // Create a fresh env with closure bindings, executing in the defining namespace.
498    // This ensures macros qualify symbols relative to their definition site.
499    let mut env = Env::with_closure(caller_env.globals.clone(), &f.defining_ns, f);
500
501    let mut current_args = Vec::from(args);
502    loop {
503        // Root current_args on the shadow stack so they survive GC.
504        // They haven't been bound into the env yet.
505        let _args_root = crate::env::gc_roots::root_values(&current_args);
506
507        // GC safepoint before entering function body
508        crate::env::gc_roots::gc_safepoint(&env);
509
510        env.push_frame();
511
512        // Under GC: scope this call's heap allocations in a fresh alloc frame.
513        // Everything the body (and parameter binding) allocates is rooted only
514        // until the frame drops at the end of this trampoline iteration, so a
515        // deep call's locals and a `recur`'s dead intermediates become
516        // collectable instead of being pinned for the lifetime of the enclosing
517        // top-level form.  `result` is moved out before the frame drops and is
518        // re-rooted at the top of the next iteration (`root_values`) or by the
519        // caller during return unwinding — no GC safepoint runs in the
520        // interval, exactly as the IR/JIT dispatch seam relies on (below).
521        #[cfg(not(feature = "no-gc"))]
522        let _call_frame = cljrs_gc::push_alloc_frame();
523
524        // Bind params.
525        bind_fn_params(arity, &current_args, &mut env)?;
526
527        // Self-reference for named functions: use self_ptr when available so
528        // the binding is pointer-equal to the outer Value::Fn holding this fn.
529        if let Some(ref name) = f.name {
530            let self_val = if let Some(ref p) = f.self_ptr {
531                Value::Fn(p.clone())
532            } else {
533                Value::Fn(GcPtr::new(f.clone()))
534            };
535            env.bind(name.clone(), self_val);
536        }
537
538        // Eval body, catching Recur.
539        // Under no-gc: push a scratch region; evaluate all-but-last in it,
540        // then pop scratch before the tail expression so the return value
541        // lands in the caller's allocation context.
542        #[cfg(not(feature = "no-gc"))]
543        let result = eval_body_recur_fn(&arity.body, &mut env);
544        #[cfg(feature = "no-gc")]
545        let result = {
546            let mut scratch = cljrs_gc::alloc_ctx::ScratchGuard::new();
547            // scratch drops here: resets the region (frees intermediates)
548            eval_body_with_scratch(&arity.body, &mut scratch, &mut env)
549        };
550        env.pop_frame();
551        // _call_frame drops at the end of this iteration (after the match
552        // below), freeing this call's intermediates.
553
554        match result {
555            Ok(v) => return Ok(v),
556            Err(EvalError::Recur(new_args)) => {
557                // For variadic arities, recur provides n+1 values where the
558                // last value IS the rest collection (not spread args to be
559                // re-collected). Flatten it so bind_fn_params sees the right
560                // number of individual args.
561                if arity.rest_param.is_some() {
562                    let n = arity.params.len();
563                    if new_args.len() == n + 1 {
564                        let mut flat = new_args[..n].to_vec();
565                        // Spread the rest collection back into individual args.
566                        let rest_val = &new_args[n];
567                        match rest_val {
568                            Value::Nil => {} // no extra args
569                            _ => {
570                                let rest_items = value_to_seq_vec(rest_val);
571                                flat.extend(rest_items);
572                            }
573                        }
574                        current_args = flat;
575                    } else {
576                        current_args = new_args;
577                    }
578                } else {
579                    current_args = new_args;
580                }
581            }
582            Err(e) => return Err(e),
583        }
584    }
585}
586
587/// Bind function parameters in the current (top) frame.
588pub fn bind_fn_params(arity: &CljxFnArity, args: &[Value], env: &mut Env) -> EvalResult<()> {
589    let n = arity.params.len();
590    // Bind positional params.
591    for (i, name) in arity.params.iter().enumerate() {
592        let val = args.get(i).cloned().unwrap_or(Value::Nil);
593        env.bind(name.clone(), val);
594    }
595    // Bind rest param.
596    if let Some(ref rest) = arity.rest_param {
597        let rest_items = args[n..].to_vec();
598        let rest_val = if rest_items.is_empty() {
599            Value::Nil
600        } else {
601            Value::List(GcPtr::new(PersistentList::from_iter(rest_items)))
602        };
603        env.bind(rest.clone(), rest_val.clone());
604        // Apply rest destructuring if present.
605        if let Some(ref pattern) = arity.destructure_rest {
606            // When the rest pattern is a map destructure (e.g. `& {:keys [bar]}`),
607            // convert the rest args list into a map of alternating key-value pairs,
608            // matching Clojure's keyword-arguments convention.
609            let destructure_val = if matches!(pattern.kind, FormKind::Map(_)) {
610                let items = value_to_seq_vec(&rest_val);
611                Value::Map(MapValue::from_flat_entries(items))
612            } else {
613                rest_val
614            };
615            crate::interp::destructure::bind_pattern(pattern, destructure_val, env)?;
616        }
617    }
618    // Apply positional destructuring patterns.
619    for (idx, pattern) in &arity.destructure_params {
620        let val = args.get(*idx).cloned().unwrap_or(Value::Nil);
621        crate::interp::destructure::bind_pattern(pattern, val, env)?;
622    }
623    Ok(())
624}
625
626/// Eval a function body, propagating Recur up (does not catch it).
627#[cfg(not(feature = "no-gc"))]
628fn eval_body_recur_fn(body: &[cljrs_reader::Form], env: &mut Env) -> EvalResult {
629    let mut result = Value::Nil;
630    for form in body {
631        result = eval(form, env)?;
632    }
633    Ok(result)
634}
635
636/// Under `no-gc`: evaluate body forms with the scratch region active for all
637/// non-tail forms, then pop the scratch before the tail expression so the
638/// return value (or `recur` args) are allocated in the caller's context.
639#[cfg(feature = "no-gc")]
640fn eval_body_with_scratch(
641    body: &[cljrs_reader::Form],
642    scratch: &mut cljrs_gc::alloc_ctx::ScratchGuard,
643    env: &mut Env,
644) -> EvalResult {
645    if body.is_empty() {
646        scratch.pop_for_return();
647        return Ok(Value::Nil);
648    }
649    // Eval all non-tail forms in the scratch region.
650    for form in &body[..body.len() - 1] {
651        eval(form, env)?;
652    }
653    // Pop scratch so the tail expression allocates in the caller's context.
654    scratch.pop_for_return();
655    eval(&body[body.len() - 1], env)
656}
657
658/// Select the matching arity for the given argument count.
659pub fn select_arity(f: &CljxFn, argc: usize) -> EvalResult<&CljxFnArity> {
660    let name = f.name.as_deref().unwrap_or("fn");
661    // Try fixed arities first.
662    for arity in &f.arities {
663        if arity.rest_param.is_none() && arity.params.len() == argc {
664            return Ok(arity);
665        }
666    }
667    // Try variadic arities.
668    for arity in &f.arities {
669        if arity.rest_param.is_some() && argc >= arity.params.len() {
670            return Ok(arity);
671        }
672    }
673    // Build expected string.
674    let expected: Vec<String> = f
675        .arities
676        .iter()
677        .map(|a| {
678            if a.rest_param.is_some() {
679                format!("{}+", a.params.len())
680            } else {
681                a.params.len().to_string()
682            }
683        })
684        .collect();
685    Err(EvalError::Arity {
686        name: name.to_string(),
687        expected: expected.join(" or "),
688        got: argc,
689    })
690}
691
692/// Expand a macro: convert unevaluated arg forms to values, call the macro fn,
693/// then convert the resulting Value back to a Form.
694///
695/// Clojure macros receive two implicit leading arguments:
696/// - `&form`: the entire call expression as a quoted value
697/// - `&env`: a map of local bindings at the call site (symbol → value)
698fn macro_apply(
699    mfn: &CljxFn,
700    func_form: &Form,
701    arg_forms: &[Form],
702    env: &mut Env,
703) -> EvalResult<Form> {
704    // Resolve ::kw forms using the caller's namespace before the macro sees them.
705    // In Clojure, ::kw is resolved at read time; we approximate that here so a
706    // macro splicing its arguments into a new form cannot re-resolve them against
707    // the macro's own namespace.
708    let resolved_args: Vec<Form> = arg_forms
709        .iter()
710        .map(|f| crate::builtins::form::resolve_auto_forms(f, env))
711        .collect::<EvalResult<Vec<Form>>>()?;
712
713    // &form: the whole call expression as a list value.
714    let form_val = {
715        let mut items = vec![form_to_value(func_form)?];
716        for f in &resolved_args {
717            items.push(form_to_value(f)?);
718        }
719        Value::List(GcPtr::new(PersistentList::from_iter(items)))
720    };
721
722    // &env: local variable bindings at call site as a map (symbol → value).
723    let env_val = {
724        let (names, vals) = env.all_local_bindings();
725        let mut m = MapValue::empty();
726        for (name, val) in names.iter().zip(vals.iter()) {
727            m = m.assoc(Value::symbol(Symbol::simple(name.as_ref())), val.clone());
728        }
729        Value::Map(m)
730    };
731
732    // Prepend &form and &env, then pass remaining arg forms as unevaluated values.
733    let mut args = vec![form_val, env_val];
734    for f in &resolved_args {
735        args.push(form_to_value(f)?);
736    }
737
738    let expanded_val = call_cljrs_fn(mfn, args.as_ref(), env)?;
739    let dummy_span = cljrs_types::span::Span::new(Arc::new("<macro>".to_string()), 0, 0, 1, 1);
740    crate::interp::macros::value_to_form(&expanded_val, dummy_span)
741}
742
743/// Handle `(apply f arg1 ... last-coll)` — spread the last arg.
744fn handle_apply_call(arg_forms: &[Form], env: &mut Env) -> EvalResult {
745    let mut evaled: Vec<Value> = Vec::with_capacity(arg_forms.len());
746    for f in arg_forms {
747        let _root = crate::env::gc_roots::root_values(&evaled);
748        evaled.push(eval(f, env)?);
749    }
750
751    if evaled.len() < 2 {
752        return Err(EvalError::Arity {
753            name: "apply".into(),
754            expected: "2+".into(),
755            got: evaled.len(),
756        });
757    }
758
759    let f = evaled.remove(0);
760    let last = evaled.pop().unwrap();
761    // Root f, last, and remaining evaled args during spread (which may realize lazy seqs).
762    let _f_root = crate::env::gc_roots::root_value(&f);
763    let _last_root = crate::env::gc_roots::root_value(&last);
764    let _evaled_root = crate::env::gc_roots::root_values(&evaled);
765    // Spread last arg.
766    let spread = value_to_seq_vec(&last);
767    evaled.extend(spread);
768    crate::env::apply::apply_value(&f, evaled, env)
769}
770
771/// Handle `(make-lazy-seq f)` — wraps a zero-arg fn in a lazy sequence.
772pub fn handle_make_lazy_seq(arg_forms: &[Form], env: &mut Env) -> EvalResult {
773    if arg_forms.len() != 1 {
774        return Err(EvalError::Arity {
775            name: "make-lazy-seq".into(),
776            expected: "1".into(),
777            got: arg_forms.len(),
778        });
779    }
780    let f_val = eval(&arg_forms[0], env)?;
781    let f = match f_val {
782        Value::Fn(f) => f.get().clone(),
783        other => {
784            return Err(EvalError::Runtime(format!(
785                "make-lazy-seq requires a fn, got {}",
786                other.type_name()
787            )));
788        }
789    };
790    let thunk = ClosureThunk {
791        f,
792        globals: env.globals.clone(),
793        ns: env.current_ns.clone(),
794    };
795    Ok(Value::LazySeq(GcPtr::new(LazySeq::new(Box::new(thunk)))))
796}
797
798/// Handle `(make-delay f)` — wraps a zero-arg fn in a Delay.
799fn handle_make_delay(arg_forms: &[Form], env: &mut Env) -> EvalResult {
800    if arg_forms.len() != 1 {
801        return Err(EvalError::Arity {
802            name: "make-delay".into(),
803            expected: "1".into(),
804            got: arg_forms.len(),
805        });
806    }
807    let f_val = eval(&arg_forms[0], env)?;
808    let f = match f_val {
809        Value::Fn(f) => f.get().clone(),
810        other => {
811            return Err(EvalError::Runtime(format!(
812                "make-delay requires a fn, got {}",
813                other.type_name()
814            )));
815        }
816    };
817    let thunk = ClosureThunk {
818        f,
819        globals: env.globals.clone(),
820        ns: env.current_ns.clone(),
821    };
822    Ok(Value::Delay(GcPtr::new(Delay::new(Box::new(thunk)))))
823}
824
825/// Handle `(vswap! vol f & args)` — apply f to current volatile value and store.
826fn handle_vswap(arg_forms: &[Form], env: &mut Env) -> EvalResult {
827    if arg_forms.len() < 2 {
828        return Err(EvalError::Arity {
829            name: "vswap!".into(),
830            expected: "2+".into(),
831            got: arg_forms.len(),
832        });
833    }
834    let vol_val = eval(&arg_forms[0], env)?;
835    let f = eval(&arg_forms[1], env)?;
836    let extra: Vec<Value> = arg_forms[2..]
837        .iter()
838        .map(|a| eval(a, env))
839        .collect::<EvalResult<_>>()?;
840
841    match vol_val {
842        Value::Volatile(v) => {
843            let cur = v.get().deref();
844            let mut call_args = vec![cur];
845            call_args.extend(extra);
846            // Under no-gc: the value written into the volatile must live in the
847            // StaticArena since the volatile outlives all scratch regions.
848            #[cfg(feature = "no-gc")]
849            let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
850            let new_val = crate::env::apply::apply_value(&f, call_args, env)?;
851            v.get().reset(new_val.clone());
852            Ok(new_val)
853        }
854        other => Err(EvalError::Runtime(format!(
855            "vswap!: expected volatile, got {}",
856            other.type_name()
857        ))),
858    }
859}
860
861// ── volatile! ────────────────────────────────────────────────────────────────
862
863/// Handle `(volatile! init-val)`.
864fn handle_volatile(arg_forms: &[Form], env: &mut Env) -> EvalResult {
865    if arg_forms.is_empty() {
866        return Err(EvalError::Arity {
867            name: "volatile!".into(),
868            expected: "1".into(),
869            got: 0,
870        });
871    }
872    // Under no-gc: volatile initial value must live in the StaticArena since
873    // the Volatile container outlives all scratch regions.
874    #[cfg(feature = "no-gc")]
875    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
876    let initial = eval(&arg_forms[0], env)?;
877    Ok(Value::Volatile(GcPtr::new(Volatile::new(initial))))
878}
879
880// ── vreset! ──────────────────────────────────────────────────────────────────
881
882/// Handle `(vreset! vol new-val)`.
883fn handle_vreset(arg_forms: &[Form], env: &mut Env) -> EvalResult {
884    if arg_forms.len() < 2 {
885        return Err(EvalError::Arity {
886            name: "vreset!".into(),
887            expected: "2".into(),
888            got: arg_forms.len(),
889        });
890    }
891    let vol_val = eval(&arg_forms[0], env)?;
892    // Under no-gc: the new value written into the volatile must live in the
893    // StaticArena since the volatile outlives all scratch regions.
894    #[cfg(feature = "no-gc")]
895    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
896    let new_val = eval(&arg_forms[1], env)?;
897    match &vol_val {
898        Value::Volatile(v) => {
899            v.get().reset(new_val.clone());
900            Ok(new_val)
901        }
902        other => Err(EvalError::Runtime(format!(
903            "vreset!: expected volatile, got {}",
904            other.type_name()
905        ))),
906    }
907}
908
909// ── agent ────────────────────────────────────────────────────────────────────
910
911/// Handle `(agent init-val & opts)`.
912fn handle_agent_call(_arg_forms: &[Form], _env: &mut Env) -> EvalResult {
913    Err(EvalError::Runtime("agent is not yet implemented".into()))
914}
915
916/// Handle `(send agent f & extra)` / `(send-off agent f & extra)`.
917fn handle_send(_arg_forms: &[Form], _env: &mut Env) -> EvalResult {
918    Err(EvalError::Runtime(
919        "send/send-off: agents are not yet implemented".into(),
920    ))
921}
922
923// ── atom ──────────────────────────────────────────────────────────────────────
924
925/// Handle `(swap! atom f & args)`.
926fn handle_atom_call(arg_forms: &[Form], env: &mut Env) -> EvalResult {
927    if arg_forms.is_empty() {
928        return Err(EvalError::Arity {
929            name: "atom".into(),
930            expected: "1+".into(),
931            got: 0,
932        });
933    }
934    // Under no-gc: atom initial value must live in the StaticArena since the
935    // Atom container outlives all scratch regions.
936    #[cfg(feature = "no-gc")]
937    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
938    let initial = eval(&arg_forms[0], env)?;
939
940    // Evaluate and parse keyword options; unknown keys / nil keys are ignored.
941    let options: Vec<Value> = arg_forms[1..]
942        .iter()
943        .map(|f| eval(f, env))
944        .collect::<EvalResult<_>>()?;
945
946    let mut meta_opt: Option<Value> = None;
947    let mut validator_opt: Option<Value> = None;
948    let mut i = 0;
949    while i + 1 < options.len() {
950        match &options[i] {
951            Value::Keyword(k) if k.get().name.as_ref() == "meta" => {
952                meta_opt = Some(options[i + 1].clone());
953                i += 2;
954            }
955            Value::Keyword(k) if k.get().name.as_ref() == "validator" => {
956                let vf = options[i + 1].clone();
957                validator_opt = if vf == Value::Nil { None } else { Some(vf) };
958                i += 2;
959            }
960            _ => {
961                i += 2;
962            }
963        }
964    }
965
966    // Validate :meta must be nil or a map.
967    if let Some(ref m) = meta_opt
968        && !matches!(m, Value::Nil | Value::Map(_))
969    {
970        return Err(EvalError::Thrown(Value::string(
971            "Atom metadata must be a map or nil".to_string(),
972        )));
973    }
974
975    // Check validator on the initial value.
976    if let Some(ref vf) = validator_opt {
977        let result = crate::env::apply::apply_value(vf, vec![initial.clone()], env)?;
978        if result == Value::Nil || result == Value::Bool(false) {
979            return Err(EvalError::Thrown(Value::string(
980                "Invalid initial value for atom".to_string(),
981            )));
982        }
983    }
984
985    let atom = GcPtr::new(Atom::new(initial));
986    if let Some(m) = meta_opt {
987        atom.get()
988            .set_meta(if m == Value::Nil { None } else { Some(m) });
989    }
990    if let Some(vf) = validator_opt {
991        atom.get().set_validator(Some(vf));
992    }
993    Ok(Value::Atom(atom))
994}
995
996// ── shared-atom (Phase B3, two-tier ADR) ──────────────────────────────────────
997//
998// `shared-atom` is the cross-isolate tier of the two-tier atom design: its
999// contents live in `SharedValue` (Send + Sync, refcounted) behind a lock-free
1000// `ArcSwap`, so the same atom can be observed and mutated from any isolate.
1001// `deref`/`reset!`/`swap!`/`compare-and-set!` all route through these helpers
1002// when handed a `Value::SharedAtom`, so the surface mirrors a local `atom`
1003// except that values are promoted on write and demoted on read.
1004
1005/// `reset!` on a shared-atom: promote the new value and store it atomically.
1006/// Returns the (isolate-local) value that was written.
1007fn shared_atom_reset(sa: &Arc<cljrs_value::SharedAtom>, new_val: Value) -> EvalResult {
1008    let promoted = cljrs_value::promote(&new_val).map_err(|e| EvalError::Runtime(e.to_string()))?;
1009    sa.reset(promoted);
1010    Ok(new_val)
1011}
1012
1013/// `swap!` on a shared-atom: CAS-retry loop.  Loads the current value, demotes
1014/// it into an isolate-local `Value`, applies `f` (plus any extra args), promotes
1015/// the result, and commits with a single compare-and-set — retrying from the
1016/// fresh value if another isolate raced us in between.
1017fn shared_atom_swap(
1018    sa: &Arc<cljrs_value::SharedAtom>,
1019    f: &Value,
1020    extra: Vec<Value>,
1021    env: &mut Env,
1022) -> EvalResult {
1023    loop {
1024        let cur = sa.deref_val();
1025        let old_val = cljrs_value::demote(&cur);
1026        let mut call_args = Vec::with_capacity(1 + extra.len());
1027        call_args.push(old_val);
1028        call_args.extend(extra.iter().cloned());
1029        let new_val = crate::env::apply::apply_value(f, call_args, env)?;
1030        let promoted =
1031            cljrs_value::promote(&new_val).map_err(|e| EvalError::Runtime(e.to_string()))?;
1032        if sa.compare_and_set(&cur, promoted) {
1033            return Ok(new_val);
1034        }
1035        // Lost the race; another writer committed first. Re-read and retry.
1036    }
1037}
1038
1039// ── reset! ────────────────────────────────────────────────────────────────────
1040
1041fn handle_reset_bang(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1042    if arg_forms.len() < 2 {
1043        return Err(EvalError::Arity {
1044            name: "reset!".into(),
1045            expected: "2".into(),
1046            got: arg_forms.len(),
1047        });
1048    }
1049    let atom_val = eval(&arg_forms[0], env)?;
1050    // Under no-gc: the new value written into the atom must live in the
1051    // StaticArena since the atom outlives all scratch regions.
1052    #[cfg(feature = "no-gc")]
1053    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1054    let new_val = eval(&arg_forms[1], env)?;
1055
1056    let atom = match &atom_val {
1057        Value::Atom(a) => a.clone(),
1058        Value::SharedAtom(sa) => return shared_atom_reset(sa, new_val),
1059        v => {
1060            return Err(EvalError::Runtime(format!(
1061                "reset! requires an atom, got {}",
1062                v.type_name()
1063            )));
1064        }
1065    };
1066
1067    validate_atom_value(&atom, &new_val, env)?;
1068    let old_val = atom.get().deref();
1069    atom.get().reset(new_val.clone());
1070    fire_watches(&atom.get().watches, &atom_val, &old_val, &new_val, env);
1071    check_watch_error()?;
1072    Ok(new_val)
1073}
1074
1075/// Call the atom's validator (if any) on `new_val`. Throws if invalid.
1076fn validate_atom_value(atom: &GcPtr<Atom>, new_val: &Value, env: &mut Env) -> EvalResult<()> {
1077    if let Some(vf) = atom.get().get_validator() {
1078        let result = crate::env::apply::apply_value(&vf, vec![new_val.clone()], env)?;
1079        if result == Value::Nil || result == Value::Bool(false) {
1080            return Err(EvalError::Thrown(Value::string(
1081                "Invalid value for atom".to_string(),
1082            )));
1083        }
1084    }
1085    Ok(())
1086}
1087
1088// ── swap! ─────────────────────────────────────────────────────────────────────
1089
1090fn handle_swap_call(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1091    let mut evaled: Vec<Value> = arg_forms
1092        .iter()
1093        .map(|f| eval(f, env))
1094        .collect::<EvalResult<_>>()?;
1095
1096    if evaled.len() < 2 {
1097        return Err(EvalError::Arity {
1098            name: "swap!".into(),
1099            expected: "2+".into(),
1100            got: evaled.len(),
1101        });
1102    }
1103
1104    let atom_val = evaled.remove(0);
1105    let f = evaled.remove(0);
1106
1107    let atom = match &atom_val {
1108        Value::Atom(a) => a.clone(),
1109        Value::SharedAtom(sa) => return shared_atom_swap(sa, &f, evaled, env),
1110        v => {
1111            return Err(EvalError::Runtime(format!(
1112                "swap! requires an atom, got {}",
1113                v.type_name()
1114            )));
1115        }
1116    };
1117
1118    let old_val = atom.get().deref();
1119    let mut args = vec![old_val.clone()];
1120    args.extend(evaled);
1121    // Under no-gc: the value written into the atom must live in the StaticArena
1122    // since the atom outlives all scratch regions.
1123    #[cfg(feature = "no-gc")]
1124    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1125    let new_val = crate::env::apply::apply_value(&f, args, env)?;
1126    validate_atom_value(&atom, &new_val, env)?;
1127    atom.get().reset(new_val.clone());
1128    fire_watches(&atom.get().watches, &atom_val, &old_val, &new_val, env);
1129    check_watch_error()?;
1130    Ok(new_val)
1131}
1132
1133// ── with-bindings* ────────────────────────────────────────────────────────────
1134
1135/// `(with-bindings* {#'var val ...} fn)` — push a binding frame, call fn with
1136/// no args, pop the frame, return the result.
1137fn handle_with_bindings(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1138    if arg_forms.len() < 2 {
1139        return Err(EvalError::Arity {
1140            name: "with-bindings*".into(),
1141            expected: "2".into(),
1142            got: arg_forms.len(),
1143        });
1144    }
1145    let map_val = eval(&arg_forms[0], env)?;
1146    let func_val = eval(&arg_forms[1], env)?;
1147
1148    let mut frame: HashMap<usize, Value> = HashMap::new();
1149    if let Value::Map(m) = &map_val {
1150        m.for_each(|k, v| {
1151            if let Value::Var(vp) = k {
1152                frame.insert(crate::env::dynamics::var_key_of(vp), v.clone());
1153            }
1154            // non-Var keys silently ignored
1155        });
1156    } else {
1157        return Err(EvalError::Runtime(
1158            "with-bindings*: first arg must be a map".into(),
1159        ));
1160    }
1161
1162    let _guard = crate::env::dynamics::push_frame(frame);
1163    crate::env::apply::apply_value(&func_val, vec![], env)
1164}
1165
1166// ── alter-var-root ────────────────────────────────────────────────────────────
1167
1168/// `(alter-var-root #'v f & args)` — atomically apply `f` to the root value.
1169fn handle_alter_var_root(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1170    if arg_forms.len() < 2 {
1171        return Err(EvalError::Arity {
1172            name: "alter-var-root".into(),
1173            expected: "2+".into(),
1174            got: arg_forms.len(),
1175        });
1176    }
1177    let var_val = eval(&arg_forms[0], env)?;
1178    let f = eval(&arg_forms[1], env)?;
1179    let extra: Vec<Value> = arg_forms[2..]
1180        .iter()
1181        .map(|form| eval(form, env))
1182        .collect::<EvalResult<_>>()?;
1183
1184    let vp = match &var_val {
1185        Value::Var(vp) => vp.clone(),
1186        v => {
1187            return Err(EvalError::Runtime(format!(
1188                "alter-var-root: expected var, got {}",
1189                v.type_name()
1190            )));
1191        }
1192    };
1193    let old_val = vp.get().deref().unwrap_or(Value::Nil);
1194    let mut call_args = vec![old_val.clone()];
1195    call_args.extend(extra);
1196    // Under no-gc: the new Var root value must live in the StaticArena since
1197    // Vars outlive all scratch regions.
1198    #[cfg(feature = "no-gc")]
1199    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1200    let new_val = crate::env::apply::apply_value(&f, call_args, env)?;
1201    vp.get().bind(new_val.clone());
1202    fire_watches(&vp.get().watches, &var_val, &old_val, &new_val, env);
1203    check_watch_error()?;
1204    Ok(new_val)
1205}
1206
1207// ── vary-meta ────────────────────────────────────────────────────────────────
1208
1209/// `(vary-meta obj f & args)` — apply `f` to obj's metadata, store result as new meta.
1210fn handle_vary_meta(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1211    if arg_forms.len() < 2 {
1212        return Err(EvalError::Arity {
1213            name: "vary-meta".into(),
1214            expected: "2+".into(),
1215            got: arg_forms.len(),
1216        });
1217    }
1218    let obj = eval(&arg_forms[0], env)?;
1219    let f = eval(&arg_forms[1], env)?;
1220    let extra: Vec<Value> = arg_forms[2..]
1221        .iter()
1222        .map(|form| eval(form, env))
1223        .collect::<EvalResult<_>>()?;
1224
1225    let current_meta = match &obj {
1226        Value::Var(vp) => vp.get().get_meta().unwrap_or(Value::Nil),
1227        _ => Value::Nil,
1228    };
1229    let mut call_args = vec![current_meta];
1230    call_args.extend(extra);
1231    let new_meta = crate::env::apply::apply_value(&f, call_args, env)?;
1232    if let Value::Var(vp) = &obj {
1233        vp.get().set_meta(new_meta);
1234    }
1235    Ok(obj)
1236}
1237
1238// ── Value-level special form dispatch (used by IR interpreter) ───────────────
1239//
1240// These mirror the `handle_*` functions above but accept already-evaluated
1241// `Vec<Value>` instead of `&[Form]`.  The IR interpreter calls these directly
1242// to bypass the sentinel stubs registered in clojure.core.
1243
1244/// Execute `reset!` with already-evaluated args: `[atom, new-val]`.
1245pub fn eval_reset_bang(args: Vec<Value>, env: &mut Env) -> EvalResult {
1246    if args.len() < 2 {
1247        return Err(EvalError::Arity {
1248            name: "reset!".into(),
1249            expected: "2".into(),
1250            got: args.len(),
1251        });
1252    }
1253    let atom_val = args[0].clone();
1254    let new_val = args[1].clone();
1255    let atom = match &atom_val {
1256        Value::Atom(a) => a.clone(),
1257        Value::SharedAtom(sa) => return shared_atom_reset(sa, new_val),
1258        v => {
1259            return Err(EvalError::Runtime(format!(
1260                "reset! requires an atom, got {}",
1261                v.type_name()
1262            )));
1263        }
1264    };
1265    #[cfg(feature = "no-gc")]
1266    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1267    validate_atom_value(&atom, &new_val, env)?;
1268    let old_val = atom.get().deref();
1269    atom.get().reset(new_val.clone());
1270    fire_watches(&atom.get().watches, &atom_val, &old_val, &new_val, env);
1271    check_watch_error()?;
1272    Ok(new_val)
1273}
1274
1275/// Execute `swap!` with already-evaluated args: `[atom, f, extra...]`.
1276pub fn eval_swap_bang(mut args: Vec<Value>, env: &mut Env) -> EvalResult {
1277    if args.len() < 2 {
1278        return Err(EvalError::Arity {
1279            name: "swap!".into(),
1280            expected: "2+".into(),
1281            got: args.len(),
1282        });
1283    }
1284    let atom_val = args.remove(0);
1285    let f = args.remove(0);
1286    let atom = match &atom_val {
1287        Value::Atom(a) => a.clone(),
1288        Value::SharedAtom(sa) => return shared_atom_swap(sa, &f, args, env),
1289        v => {
1290            return Err(EvalError::Runtime(format!(
1291                "swap! requires an atom, got {}",
1292                v.type_name()
1293            )));
1294        }
1295    };
1296    let old_val = atom.get().deref();
1297    let mut call_args = vec![old_val.clone()];
1298    call_args.extend(args);
1299    #[cfg(feature = "no-gc")]
1300    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1301    let new_val = crate::env::apply::apply_value(&f, call_args, env)?;
1302    validate_atom_value(&atom, &new_val, env)?;
1303    atom.get().reset(new_val.clone());
1304    fire_watches(&atom.get().watches, &atom_val, &old_val, &new_val, env);
1305    check_watch_error()?;
1306    Ok(new_val)
1307}
1308
1309/// Execute `volatile!` with already-evaluated args: `[init-val]`.
1310pub fn eval_volatile(args: Vec<Value>) -> EvalResult {
1311    if args.is_empty() {
1312        return Err(EvalError::Arity {
1313            name: "volatile!".into(),
1314            expected: "1".into(),
1315            got: 0,
1316        });
1317    }
1318    #[cfg(feature = "no-gc")]
1319    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1320    Ok(Value::Volatile(GcPtr::new(Volatile::new(
1321        args.into_iter().next().unwrap(),
1322    ))))
1323}
1324
1325/// Execute `vreset!` with already-evaluated args: `[volatile, new-val]`.
1326pub fn eval_vreset_bang(args: Vec<Value>) -> EvalResult {
1327    if args.len() < 2 {
1328        return Err(EvalError::Arity {
1329            name: "vreset!".into(),
1330            expected: "2".into(),
1331            got: args.len(),
1332        });
1333    }
1334    #[cfg(feature = "no-gc")]
1335    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1336    let new_val = args[1].clone();
1337    match &args[0] {
1338        Value::Volatile(v) => {
1339            v.get().reset(new_val.clone());
1340            Ok(new_val)
1341        }
1342        other => Err(EvalError::Runtime(format!(
1343            "vreset!: expected volatile, got {}",
1344            other.type_name()
1345        ))),
1346    }
1347}
1348
1349/// Execute `vswap!` with already-evaluated args: `[volatile, f, extra...]`.
1350pub fn eval_vswap_bang(mut args: Vec<Value>, env: &mut Env) -> EvalResult {
1351    if args.len() < 2 {
1352        return Err(EvalError::Arity {
1353            name: "vswap!".into(),
1354            expected: "2+".into(),
1355            got: args.len(),
1356        });
1357    }
1358    let vol_val = args.remove(0);
1359    let f = args.remove(0);
1360    match vol_val {
1361        Value::Volatile(v) => {
1362            let cur = v.get().deref();
1363            let mut call_args = vec![cur];
1364            call_args.extend(args);
1365            #[cfg(feature = "no-gc")]
1366            let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1367            let new_val = crate::env::apply::apply_value(&f, call_args, env)?;
1368            v.get().reset(new_val.clone());
1369            Ok(new_val)
1370        }
1371        other => Err(EvalError::Runtime(format!(
1372            "vswap!: expected volatile, got {}",
1373            other.type_name()
1374        ))),
1375    }
1376}
1377
1378/// Wrap a zero-arg callable in a `Value::Delay`.
1379///
1380/// Analogous to [`make_lazy_seq_from_fn`] but produces a `Delay` instead of
1381/// a `LazySeq`.
1382pub fn make_delay_from_fn(
1383    f_val: &Value,
1384    globals: std::sync::Arc<crate::env::env::GlobalEnv>,
1385    ns: std::sync::Arc<str>,
1386) -> EvalResult {
1387    let f = match f_val {
1388        Value::Fn(f) => f.get().clone(),
1389        other => {
1390            return Err(EvalError::Runtime(format!(
1391                "make-delay requires a fn, got {}",
1392                other.type_name()
1393            )));
1394        }
1395    };
1396    let thunk = ClosureThunk { f, globals, ns };
1397    Ok(Value::Delay(GcPtr::new(Delay::new(Box::new(thunk)))))
1398}
1399
1400/// Execute `alter-var-root` with already-evaluated args: `[var, f, extra...]`.
1401pub fn eval_alter_var_root(mut args: Vec<Value>, env: &mut Env) -> EvalResult {
1402    if args.len() < 2 {
1403        return Err(EvalError::Arity {
1404            name: "alter-var-root".into(),
1405            expected: "2+".into(),
1406            got: args.len(),
1407        });
1408    }
1409    let var_val = args.remove(0);
1410    let f = args.remove(0);
1411    let vp = match &var_val {
1412        Value::Var(vp) => vp.clone(),
1413        v => {
1414            return Err(EvalError::Runtime(format!(
1415                "alter-var-root: expected var, got {}",
1416                v.type_name()
1417            )));
1418        }
1419    };
1420    let old_val = vp.get().deref().unwrap_or(Value::Nil);
1421    let mut call_args = vec![old_val.clone()];
1422    call_args.extend(args);
1423    #[cfg(feature = "no-gc")]
1424    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1425    let new_val = crate::env::apply::apply_value(&f, call_args, env)?;
1426    vp.get().bind(new_val.clone());
1427    fire_watches(&vp.get().watches, &var_val, &old_val, &new_val, env);
1428    check_watch_error()?;
1429    Ok(new_val)
1430}
1431
1432/// Execute `vary-meta` with already-evaluated args: `[obj, f, extra...]`.
1433pub fn eval_vary_meta(mut args: Vec<Value>, env: &mut Env) -> EvalResult {
1434    if args.len() < 2 {
1435        return Err(EvalError::Arity {
1436            name: "vary-meta".into(),
1437            expected: "2+".into(),
1438            got: args.len(),
1439        });
1440    }
1441    let obj = args.remove(0);
1442    let f = args.remove(0);
1443    let current_meta = match &obj {
1444        Value::Var(vp) => vp.get().get_meta().unwrap_or(Value::Nil),
1445        _ => Value::Nil,
1446    };
1447    let mut call_args = vec![current_meta];
1448    call_args.extend(args);
1449    let new_meta = crate::env::apply::apply_value(&f, call_args, env)?;
1450    if let Value::Var(vp) = &obj {
1451        vp.get().set_meta(new_meta);
1452    }
1453    Ok(obj)
1454}
1455
1456/// Execute `with-bindings*` with already-evaluated args: `[bindings-map, f]`.
1457pub fn eval_with_bindings_star(args: Vec<Value>, env: &mut Env) -> EvalResult {
1458    if args.len() < 2 {
1459        return Err(EvalError::Arity {
1460            name: "with-bindings*".into(),
1461            expected: "2".into(),
1462            got: args.len(),
1463        });
1464    }
1465    let mut frame: HashMap<usize, Value> = HashMap::new();
1466    if let Value::Map(m) = &args[0] {
1467        m.for_each(|k, v| {
1468            if let Value::Var(vp) = k {
1469                frame.insert(crate::env::dynamics::var_key_of(vp), v.clone());
1470            }
1471        });
1472    } else {
1473        return Err(EvalError::Runtime(
1474            "with-bindings*: first arg must be a map".into(),
1475        ));
1476    }
1477    let _guard = crate::env::dynamics::push_frame(frame);
1478    crate::env::apply::apply_value(&args[1], vec![], env)
1479}
1480
1481/// Execute `send` / `send-off` with already-evaluated args: `[agent, f, extra...]`.
1482pub fn eval_send_to_agent(_args: Vec<Value>, _env: &mut Env) -> EvalResult {
1483    Err(EvalError::Runtime(
1484        "send/send-off: agents are not yet implemented".into(),
1485    ))
1486}
1487
1488// ── Namespace reflection (env-needing) ────────────────────────────────────────
1489
1490fn ns_name_from_val(v: &Value) -> Result<String, EvalError> {
1491    match v {
1492        Value::Symbol(s) => Ok(s.get().name.as_ref().to_string()),
1493        Value::Str(s) => Ok(s.get().clone()),
1494        Value::Namespace(ns) => Ok(ns.get().name.as_ref().to_string()),
1495        Value::Keyword(k) => Ok(k.get().name.as_ref().to_string()),
1496        other => Err(EvalError::Runtime(format!(
1497            "expected symbol, string, or namespace, got {}",
1498            other.type_name()
1499        ))),
1500    }
1501}
1502
1503/// Resolve an already-evaluated arg to a `Namespace`, matching Clojure's
1504/// `the-ns`: pass a `Namespace` through unchanged, otherwise resolve a
1505/// symbol/string/keyword name against the global namespace table, throwing
1506/// if there's no such namespace (rather than a "wrong type" error).
1507fn the_ns(v: &Value, env: &Env) -> Result<GcPtr<cljrs_value::Namespace>, EvalError> {
1508    if let Value::Namespace(ns) = v {
1509        return Ok(ns.clone());
1510    }
1511    let name = ns_name_from_val(v)?;
1512    let map = env.globals.namespaces.read().unwrap();
1513    match map.get(name.as_str()) {
1514        Some(ns) => Ok(ns.clone()),
1515        None => Err(EvalError::Runtime(format!("No namespace: {name} found"))),
1516    }
1517}
1518
1519/// `(ns-interns ns)` / `(ns-publics ns)` — map of unqualified Symbol → Var
1520/// for all interned vars. Accepts a namespace, symbol, or string (via `the-ns`).
1521fn handle_ns_interns(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1522    if arg_forms.is_empty() {
1523        return Err(EvalError::Arity {
1524            name: "ns-interns".into(),
1525            expected: "1".into(),
1526            got: 0,
1527        });
1528    }
1529    let arg = eval(&arg_forms[0], env)?;
1530    let ns = the_ns(&arg, env)?;
1531    crate::builtins::builtins::builtin_ns_interns(&[Value::Namespace(ns)])
1532        .map_err(crate::env::error::value_error_to_eval_error)
1533}
1534
1535/// `(ns-refers ns)` — map of Symbol → Var for all referred vars.
1536fn handle_ns_refers(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1537    if arg_forms.is_empty() {
1538        return Err(EvalError::Arity {
1539            name: "ns-refers".into(),
1540            expected: "1".into(),
1541            got: 0,
1542        });
1543    }
1544    let arg = eval(&arg_forms[0], env)?;
1545    let ns = the_ns(&arg, env)?;
1546    crate::builtins::builtins::builtin_ns_refers(&[Value::Namespace(ns)])
1547        .map_err(crate::env::error::value_error_to_eval_error)
1548}
1549
1550/// `(ns-map ns)` — map of Symbol → Var for all visible names (interns + refers).
1551fn handle_ns_map(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1552    if arg_forms.is_empty() {
1553        return Err(EvalError::Arity {
1554            name: "ns-map".into(),
1555            expected: "1".into(),
1556            got: 0,
1557        });
1558    }
1559    let arg = eval(&arg_forms[0], env)?;
1560    let ns = the_ns(&arg, env)?;
1561    crate::builtins::builtins::builtin_ns_map(&[Value::Namespace(ns)])
1562        .map_err(crate::env::error::value_error_to_eval_error)
1563}
1564
1565/// `(find-ns sym)` / `(the-ns sym)` — look up a namespace by name; nil if not found.
1566fn handle_find_ns(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1567    if arg_forms.is_empty() {
1568        return Err(EvalError::Arity {
1569            name: "find-ns".into(),
1570            expected: "1".into(),
1571            got: 0,
1572        });
1573    }
1574    let arg = eval(&arg_forms[0], env)?;
1575    let name = ns_name_from_val(&arg)?;
1576    let map = env.globals.namespaces.read().unwrap();
1577    match map.get(name.as_str()) {
1578        Some(ns) => Ok(Value::Namespace(ns.clone())),
1579        None => Ok(Value::Nil),
1580    }
1581}
1582
1583/// `(all-ns)` — lazy sequence of all live namespaces.
1584fn handle_all_ns(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1585    if !arg_forms.is_empty() {
1586        let _ = eval(&arg_forms[0], env)?; // tolerate extra args
1587    }
1588    let map = env.globals.namespaces.read().unwrap();
1589    let items: Vec<Value> = map
1590        .values()
1591        .map(|ns| Value::Namespace(ns.clone()))
1592        .collect();
1593    drop(map);
1594    Ok(Value::List(cljrs_gc::GcPtr::new(
1595        cljrs_value::PersistentList::from_iter(items),
1596    )))
1597}
1598
1599/// `(create-ns sym)` — create (or return existing) namespace, return it.
1600fn handle_create_ns(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1601    if arg_forms.is_empty() {
1602        return Err(EvalError::Arity {
1603            name: "create-ns".into(),
1604            expected: "1".into(),
1605            got: 0,
1606        });
1607    }
1608    let arg = eval(&arg_forms[0], env)?;
1609    let name = ns_name_from_val(&arg)?;
1610    let ns = env.globals.get_or_create_ns(&name);
1611    Ok(Value::Namespace(ns))
1612}
1613
1614/// `(ns-aliases ns)` — map of Symbol → Namespace for all aliases in ns.
1615fn handle_ns_aliases(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1616    if arg_forms.is_empty() {
1617        return Err(EvalError::Arity {
1618            name: "ns-aliases".into(),
1619            expected: "1".into(),
1620            got: 0,
1621        });
1622    }
1623    let ns_val = eval(&arg_forms[0], env)?;
1624    let ns_name = ns_name_from_val(&ns_val)?;
1625    let map = env.globals.namespaces.read().unwrap();
1626    let ns = match map.get(ns_name.as_str()) {
1627        Some(ns) => ns.clone(),
1628        None => return Ok(Value::Map(cljrs_value::MapValue::empty())),
1629    };
1630    let aliases = ns.get().aliases.lock().unwrap().clone();
1631    drop(map);
1632    let mut m = cljrs_value::MapValue::empty();
1633    for (alias, full_ns_name) in &aliases {
1634        let sym = Value::symbol(cljrs_value::Symbol::simple(alias.clone()));
1635        let nsmap = env.globals.namespaces.read().unwrap();
1636        if let Some(target_ns) = nsmap.get(full_ns_name.as_ref()) {
1637            m = m.assoc(sym, Value::Namespace(target_ns.clone()));
1638        }
1639    }
1640    Ok(Value::Map(m))
1641}
1642
1643/// `(remove-ns sym)` — remove a namespace (returns nil; used sparingly in tests).
1644fn handle_remove_ns(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1645    if arg_forms.is_empty() {
1646        return Err(EvalError::Arity {
1647            name: "remove-ns".into(),
1648            expected: "1".into(),
1649            got: 0,
1650        });
1651    }
1652    let arg = eval(&arg_forms[0], env)?;
1653    let name = ns_name_from_val(&arg)?;
1654    env.globals
1655        .namespaces
1656        .write()
1657        .unwrap()
1658        .remove(name.as_str());
1659    Ok(Value::Nil)
1660}
1661
1662/// `(alter-meta! ref f & args)` — apply f to ref's current meta + args, store and return new meta.
1663fn handle_alter_meta(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1664    if arg_forms.len() < 2 {
1665        return Err(EvalError::Arity {
1666            name: "alter-meta!".into(),
1667            expected: "2+".into(),
1668            got: arg_forms.len(),
1669        });
1670    }
1671    let obj = eval(&arg_forms[0], env)?;
1672    let f = eval(&arg_forms[1], env)?;
1673    let extra: Vec<Value> = arg_forms[2..]
1674        .iter()
1675        .map(|form| eval(form, env))
1676        .collect::<EvalResult<_>>()?;
1677
1678    let current_meta = match &obj {
1679        Value::Var(vp) => vp
1680            .get()
1681            .get_meta()
1682            .unwrap_or(Value::Map(cljrs_value::MapValue::empty())),
1683        _ => Value::Map(cljrs_value::MapValue::empty()),
1684    };
1685    let mut call_args = vec![current_meta];
1686    call_args.extend(extra);
1687    let new_meta = crate::env::apply::apply_value(&f, call_args, env)?;
1688    if let Value::Var(vp) = &obj {
1689        vp.get().set_meta(new_meta.clone());
1690    }
1691    Ok(new_meta)
1692}
1693
1694/// `(ns-resolve ns sym)` — return the Var for sym in ns, or nil if not found.
1695fn handle_ns_resolve(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1696    if arg_forms.len() < 2 {
1697        return Err(EvalError::Arity {
1698            name: "ns-resolve".into(),
1699            expected: "2".into(),
1700            got: arg_forms.len(),
1701        });
1702    }
1703    let ns_arg = eval(&arg_forms[0], env)?;
1704    let sym_arg = eval(&arg_forms[1], env)?;
1705    let ns_name = ns_name_from_val(&ns_arg)?;
1706    let sym_name = match &sym_arg {
1707        Value::Symbol(s) => s.get().name.as_ref().to_string(),
1708        Value::Str(s) => s.get().clone(),
1709        other => {
1710            return Err(EvalError::Runtime(format!(
1711                "ns-resolve: second arg must be symbol or string, got {}",
1712                other.type_name()
1713            )));
1714        }
1715    };
1716    match env.globals.lookup_var(&ns_name, &sym_name) {
1717        Some(var_ptr) => Ok(Value::Var(var_ptr)),
1718        None => Ok(Value::Nil),
1719    }
1720}
1721
1722/// Get the namespace name from `*ns*` (dynamic var), falling back to `env.current_ns`.
1723/// This is important for `resolve` inside macros, where `env.current_ns` is the
1724/// macro's defining namespace but `*ns*` is the caller's namespace.
1725fn resolve_current_ns(env: &Env) -> Arc<str> {
1726    if let Some(var) = env.globals.lookup_var("clojure.core", "*ns*") {
1727        let val = crate::env::dynamics::deref_var(&var);
1728        if let Some(Value::Namespace(ns_ptr)) = val {
1729            return ns_ptr.get().name.clone();
1730        }
1731    }
1732    env.current_ns.clone()
1733}
1734
1735/// `(resolve sym)` — return the Var for sym in the current namespace, or nil.
1736fn handle_resolve(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1737    if arg_forms.len() != 1 {
1738        return Err(EvalError::Arity {
1739            name: "resolve".into(),
1740            expected: "1".into(),
1741            got: arg_forms.len(),
1742        });
1743    }
1744    let resolve_ns = resolve_current_ns(env);
1745    let sym_arg = eval(&arg_forms[0], env)?;
1746    let sym_name = match &sym_arg {
1747        Value::Symbol(s) => {
1748            let sym = s.get();
1749            // If qualified (ns/name), use the given ns; otherwise current ns.
1750            if let Some(ns) = &sym.namespace {
1751                let full_ns = env
1752                    .globals
1753                    .resolve_alias(&resolve_ns, ns.as_ref())
1754                    .unwrap_or_else(|| ns.clone());
1755                return Ok(
1756                    match env.globals.lookup_var_in_ns(&full_ns, sym.name.as_ref()) {
1757                        Some(var_ptr) => Value::Var(var_ptr),
1758                        None => Value::Nil,
1759                    },
1760                );
1761            }
1762            sym.name.as_ref().to_string()
1763        }
1764        Value::Str(s) => s.get().clone(),
1765        other => {
1766            return Err(EvalError::Runtime(format!(
1767                "resolve: arg must be symbol or string, got {}",
1768                other.type_name()
1769            )));
1770        }
1771    };
1772    Ok(match env.globals.lookup_var_in_ns(&resolve_ns, &sym_name) {
1773        Some(var_ptr) => Value::Var(var_ptr),
1774        None => Value::Nil,
1775    })
1776}
1777
1778fn handle_intern(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1779    if arg_forms.len() < 2 || arg_forms.len() > 3 {
1780        return Err(EvalError::Runtime("intern expects 2 or 3 arguments".into()));
1781    }
1782    let ns_val = eval(&arg_forms[0], env)?;
1783    let ns_name: Arc<str> = match &ns_val {
1784        Value::Symbol(s) => s.get().name.clone(),
1785        Value::Namespace(ns) => ns.get().name.clone(),
1786        other => {
1787            return Err(EvalError::Runtime(format!(
1788                "intern: first arg must be namespace or symbol, got {}",
1789                other.type_name()
1790            )));
1791        }
1792    };
1793    let var_name: Arc<str> = match eval(&arg_forms[1], env)? {
1794        Value::Symbol(s) => s.get().name.clone(),
1795        other => {
1796            return Err(EvalError::Runtime(format!(
1797                "intern: second arg must be symbol, got {}",
1798                other.type_name()
1799            )));
1800        }
1801    };
1802    // Namespace must already exist (Clojure throws if it doesn't)
1803    let ns = {
1804        let map = env.globals.namespaces.read().unwrap();
1805        map.get(ns_name.as_ref()).cloned()
1806    };
1807    let ns = ns.ok_or_else(|| EvalError::Runtime(format!("No namespace: {ns_name} found")))?;
1808    let var = if arg_forms.len() == 3 {
1809        // Under no-gc: interned Var values live in the StaticArena since they
1810        // are namespace-scoped and outlive all scratch regions.
1811        #[cfg(feature = "no-gc")]
1812        let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1813        let val = eval(&arg_forms[2], env)?;
1814        let mut interns = ns.get().interns.lock().unwrap();
1815        if let Some(var) = interns.get(&var_name) {
1816            var.get().bind(val);
1817            var.clone()
1818        } else {
1819            let var =
1820                cljrs_gc::GcPtr::new(cljrs_value::Var::new(ns_name.clone(), var_name.clone()));
1821            var.get().bind(val);
1822            interns.insert(var_name, var.clone());
1823            var
1824        }
1825    } else {
1826        let mut interns = ns.get().interns.lock().unwrap();
1827        if let Some(var) = interns.get(&var_name) {
1828            var.clone()
1829        } else {
1830            let var =
1831                cljrs_gc::GcPtr::new(cljrs_value::Var::new(ns_name.clone(), var_name.clone()));
1832            interns.insert(var_name, var.clone());
1833            var
1834        }
1835    };
1836    Ok(Value::Var(var))
1837}
1838
1839// ── bound-fn* ────────────────────────────────────────────────────────────────
1840
1841/// `(bound-fn* f)` — capture current dynamic bindings and wrap `f` so that
1842/// when the wrapper is called, those bindings are installed.
1843fn handle_bound_fn_star(arg_forms: &[Form], env: &mut Env) -> EvalResult {
1844    if arg_forms.len() != 1 {
1845        return Err(EvalError::Arity {
1846            name: "bound-fn*".into(),
1847            expected: "1".into(),
1848            got: arg_forms.len(),
1849        });
1850    }
1851    let f = eval(&arg_forms[0], env)?;
1852    // Merge all binding frames into a single flat frame (bottom-up so inner wins)
1853    let frames = crate::env::dynamics::capture_current();
1854    let mut merged = std::collections::HashMap::new();
1855    for frame in &frames {
1856        merged.extend(frame.iter().map(|(k, v)| (*k, v.clone())));
1857    }
1858    Ok(Value::BoundFn(cljrs_gc::GcPtr::new(cljrs_value::BoundFn {
1859        wrapped: f,
1860        captured_bindings: merged,
1861    })))
1862}