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