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