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