Skip to main content

cljrs_runtime/interp/
apply.rs

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