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