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