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