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