Skip to main content

cljrs_runtime/interp/
special.rs

1//! Special form evaluators.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use crate::builtins::form::{
7    expand_pairs, expand_reader_conds, expand_reader_conds_cow, form_to_value, resolve_auto_forms,
8    select_reader_cond,
9};
10use crate::env::env::{Env, RequireRefer, RequireSpec};
11use crate::env::error::{EvalError, EvalResult};
12use crate::env::loader::load_ns;
13use crate::interp::destructure::bind_pattern;
14use crate::interp::eval::{eval, eval_body, is_special_form};
15use cljrs_gc::GcPtr;
16use cljrs_reader::Form;
17use cljrs_reader::form::FormKind;
18use cljrs_value::error::ExceptionInfo;
19use cljrs_value::{
20    CljxFn, CljxFnArity, Keyword, MapValue, Protocol, ProtocolMethod, ReferClojureFilter, TypeHint,
21    Value, ValueError,
22};
23
24/// Dispatch to the right special-form handler.
25pub fn eval_special(head: &str, args: &[Form], env: &mut Env) -> EvalResult {
26    crate::env::policy::check_special(head)?;
27    match head {
28        "def" => eval_def(args, env),
29        "fn*" | "fn" => eval_fn(args, env),
30        "if" => eval_if(args, env),
31        "do" => eval_body(args, env),
32        "let*" | "let" => eval_let(args, env),
33        "loop*" | "loop" => eval_loop(args, env),
34        "recur" => eval_recur(args, env),
35        "quote" => eval_quote(args, env),
36        "var" => eval_var(args, env),
37        "set!" => eval_set_bang(args, env),
38        "throw" => eval_throw(args, env),
39        "try" => eval_try(args, env),
40        "defn" => eval_defn(args, env, false),
41        "defn-" => eval_defn(args, env, true),
42        "defmacro" => eval_defmacro(args, env),
43        "defonce" => eval_defonce(args, env),
44        "and" => eval_and(args, env),
45        "or" => eval_or(args, env),
46        "." => Err(EvalError::Runtime("interop not yet implemented".into())),
47        "ns" => eval_ns(args, env),
48        "require" => eval_require(args, env),
49        "letfn" => eval_letfn(args, env),
50        "in-ns" => eval_in_ns(args, env),
51        "alias" => eval_alias(args, env),
52        "protocol*" => eval_protocol_star(args, env),
53        "deftype*" => eval_deftype_star(args, env),
54        "load-file" => eval_load_file(args, env),
55        "binding" => eval_binding(args, env),
56        "with-out-str" => eval_with_out_str(args, env),
57        "await" => eval_await(args, env),
58        _ => unreachable!("unknown special form: {head}"),
59    }
60}
61
62// ── def ───────────────────────────────────────────────────────────────────────
63
64fn eval_def(args: &[Form], env: &mut Env) -> EvalResult {
65    let target = parse_def(args, env)?;
66    let val = match target.value_form {
67        Some(form) => {
68            // Under no-gc: def value expressions go to the StaticArena since the
69            // Var must outlive all scratch regions.
70            #[cfg(feature = "no-gc")]
71            let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
72            eval(form, env)?
73        }
74        None => Value::Nil,
75    };
76    intern_def(target, val, env)
77}
78
79/// A parsed `(def name "doc"? value?)`: everything but the value itself.
80///
81/// Public, together with [`parse_def`] and [`intern_def`], so the async
82/// evaluator (`cljrs-async`) can evaluate `value_form` with a yielding
83/// evaluator and still share the rest of `def`.
84pub struct DefTarget<'a> {
85    pub name: String,
86    /// Metadata from `^meta` on the name and the docstring, merged.
87    pub meta: Option<Value>,
88    /// The value expression; `None` for `(def name)`.
89    pub value_form: Option<&'a Form>,
90}
91
92/// Parse the name, metadata and docstring of a `def` without evaluating its
93/// value expression. `^{...}` metadata on the name *is* evaluated here.
94pub fn parse_def<'a>(args: &'a [Form], env: &mut Env) -> EvalResult<DefTarget<'a>> {
95    if args.is_empty() {
96        return Err(EvalError::Runtime("def requires a name".into()));
97    }
98    let (name, meta_opt) = extract_def_name(&args[0], env)?;
99    // Optional docstring: (def name "docstring" value)
100    let (docstring, val_idx) = if args.len() > 2
101        && let FormKind::Str(s) = &args[1].kind
102    {
103        (Some(s.clone()), 2)
104    } else {
105        (None, 1)
106    };
107    Ok(DefTarget {
108        name,
109        meta: merge_meta(meta_opt, docstring.as_deref().map(doc_meta)),
110        value_form: args.get(val_idx),
111    })
112}
113
114/// Intern `val` under a parsed `def` target in the current namespace.
115pub fn intern_def(target: DefTarget<'_>, val: Value, env: &mut Env) -> EvalResult {
116    let var = env
117        .globals
118        .intern(&env.current_ns, Arc::from(target.name.as_str()), val);
119    if let Some(meta_val) = target.meta {
120        var.get().set_meta(meta_val);
121    }
122    Ok(Value::Var(var))
123}
124
125/// The already-bound var a `defonce` named `name` would leave untouched, if
126/// any.
127pub fn defonce_existing(name: &str, env: &Env) -> Option<Value> {
128    env.globals
129        .lookup_var(&env.current_ns, name)
130        .filter(|var| var.get().is_bound())
131        .map(Value::Var)
132}
133
134/// Build a `{:doc "..."}` metadata map fragment for a docstring.
135fn doc_meta(doc: &str) -> Value {
136    Value::Map(MapValue::empty().assoc(
137        Value::keyword(Keyword::parse("doc")),
138        Value::string(doc.to_string()),
139    ))
140}
141
142fn private_meta() -> Value {
143    Value::Map(
144        MapValue::empty().assoc(Value::keyword(Keyword::parse("private")), Value::Bool(true)),
145    )
146}
147
148/// Build a `{:arglists ([x] [x y & more])}` metadata fragment from a
149/// `Value::Fn`/`Value::Macro`'s parsed arities. `skip` elides leading fixed
150/// params that aren't part of the public signature (defmacro's implicit
151/// `&form`/`&env`).
152fn arglists_meta(fn_val: &Value, skip: usize) -> Option<Value> {
153    let arities = match fn_val {
154        Value::Fn(f) => &f.get().arities,
155        Value::Macro(f) => &f.get().arities,
156        _ => return None,
157    };
158    let lists: Vec<Value> = arities
159        .iter()
160        .map(|a| {
161            let mut syms: Vec<Value> = a
162                .params
163                .iter()
164                .skip(skip)
165                .map(|p| Value::symbol(cljrs_value::Symbol::simple(p.as_ref())))
166                .collect();
167            if let Some(rest) = &a.rest_param {
168                syms.push(Value::symbol(cljrs_value::Symbol::simple("&")));
169                syms.push(Value::symbol(cljrs_value::Symbol::simple(rest.as_ref())));
170            }
171            Value::Vector(GcPtr::new(cljrs_value::PersistentVector::from_iter(syms)))
172        })
173        .collect();
174    Some(Value::Map(MapValue::empty().assoc(
175        Value::keyword(Keyword::parse("arglists")),
176        Value::Vector(GcPtr::new(cljrs_value::PersistentVector::from_iter(lists))),
177    )))
178}
179
180/// Extract the def name and optional metadata from the name form.
181fn extract_def_name(form: &Form, env: &mut Env) -> EvalResult<(String, Option<Value>)> {
182    match &form.kind {
183        FormKind::Symbol(s) => Ok((s.clone(), None)),
184        // `^:a ^:b x` nests `Meta` forms; unwrap all, outer mark winning.
185        FormKind::Meta(meta_form, inner) => {
186            let meta_val = compile_meta_form(meta_form, env)?;
187            let (name, inner_meta) = extract_def_name(inner, env)?;
188            Ok((name, merge_meta(inner_meta, Some(meta_val))))
189        }
190        _ => Err(EvalError::Runtime("def name must be a symbol".into())),
191    }
192}
193
194/// Expand a metadata shorthand form into a map value, evaluating the general
195/// case (`^{:x (+ 1 2)}` → `{:x 3}`).
196///
197/// The shorthand table itself lives in [`crate::builtins::form`] and is shared
198/// with the quoted position, which differs only in resolving that general case
199/// as data instead.
200pub fn compile_meta_form(meta: &Form, env: &mut Env) -> EvalResult<Value> {
201    crate::builtins::form::expand_meta_annotation(meta, &mut |f| eval(f, env))
202}
203
204// ── fn* ───────────────────────────────────────────────────────────────────────
205
206fn eval_fn(args: &[Form], env: &mut Env) -> EvalResult {
207    // `:async` may be requested on the first argument, `(fn ^:async [..] ..)`
208    // / `(fn ^:async name [..] ..)`, or on the params vector after a name,
209    // `(fn name ^:async [..] ..)` (also how `defn name ^:async [..]` arrives).
210    let is_async = Form::fn_args_request_async(args);
211    // Peel any leading `^meta` wrappers off the first argument.
212    let peeled: Vec<Form>;
213    let args: &[Form] = if matches!(args.first().map(|f| &f.kind), Some(FormKind::Meta(..))) {
214        let (_, head) = args[0].peel_meta();
215        peeled = std::iter::once(head.clone())
216            .chain(args[1..].iter().cloned())
217            .collect();
218        &peeled
219    } else {
220        args
221    };
222
223    let mut idx = 0;
224    let mut name: Option<Arc<str>> = None;
225
226    // Optional name.
227    if let Some(FormKind::Symbol(s)) = args.first().map(|f| &f.kind)
228        && !is_special_form(s)
229    {
230        name = Some(Arc::from(s.as_str()));
231        idx = 1;
232    }
233
234    let rest = &args[idx..];
235    if rest.is_empty() {
236        return Err(EvalError::Runtime("fn* requires params and body".into()));
237    }
238
239    // A `^hint` on the params vector or on an arity clause is advisory — the
240    // structural shape is the form underneath it.
241    let arities = match &rest[0].unmeta().kind {
242        FormKind::Vector(_) => {
243            // Single arity: (fn* [params] body...)
244            vec![parse_arity(&rest[0], &rest[1..])?]
245        }
246        FormKind::List(_) => {
247            // Multi-arity: (fn* ([params] body...) ...)
248            rest.iter()
249                .map(|arity_form| {
250                    if let Some(forms) = arity_form.as_list() {
251                        if forms.is_empty() {
252                            return Err(EvalError::Runtime("arity clause requires params".into()));
253                        }
254                        parse_arity(&forms[0], &forms[1..])
255                    } else {
256                        Err(EvalError::Runtime("expected arity clause (list)".into()))
257                    }
258                })
259                .collect::<EvalResult<Vec<_>>>()?
260        }
261        _ => {
262            return Err(EvalError::Runtime(
263                "fn* expects vector or arity clauses".into(),
264            ));
265        }
266    };
267
268    // Capture closed-over locals.
269    let (closed_over_names, closed_over_vals) = env.all_local_bindings();
270
271    let mut cljrs_fn = CljxFn::new(
272        name.clone(),
273        arities,
274        closed_over_names,
275        closed_over_vals,
276        false,
277        Arc::clone(&env.current_ns),
278    );
279    cljrs_fn.is_async = is_async;
280
281    env.on_fn_defined(&cljrs_fn);
282
283    // Eagerly lower each arity to IR if the compiler is ready.
284    //eager_lower_fn(&cljrs_fn, env);
285
286    let mut ptr = GcPtr::new(cljrs_fn);
287    // For named anonymous functions (fn g ...), store a back-pointer so that
288    // the self-reference returned from the body is pointer-equal to the outer
289    // binding — required for `(= f (f))` to be `true`.
290    if ptr.get().name.is_some() {
291        let self_clone = ptr.clone();
292        ptr.get_mut().self_ptr = Some(self_clone);
293    }
294    Ok(Value::Fn(ptr))
295}
296
297/// Parse one arity: params-form and body forms.
298pub fn parse_arity(params_form: &Form, body: &[Form]) -> EvalResult<CljxFnArity> {
299    let Some(param_forms) = params_form.as_vector() else {
300        return Err(EvalError::Runtime(
301            "fn arity params must be a vector".into(),
302        ));
303    };
304
305    let mut params: Vec<Arc<str>> = Vec::new();
306    let mut param_hints: Vec<Option<TypeHint>> = Vec::new();
307    let mut rest_param: Option<Arc<str>> = None;
308    let mut rest_hint: Option<TypeHint> = None;
309    let mut destructure_params: Vec<(usize, Form)> = Vec::new();
310    let mut destructure_rest: Option<Form> = None;
311    let mut saw_amp = false;
312
313    for p in param_forms {
314        // Peel a leading `^hint` (e.g. `^long x`, `^doubles a`) from the param,
315        // resolving the `:tag` to a primitive `TypeHint`.  Unknown / non-primitive
316        // tags resolve to `None` and are simply ignored (Clojure treats them as
317        // advisory).  The unwrapped form keeps the existing symbol/destructure
318        // handling unchanged.
319        let (hint, p) = peel_param_hint(p);
320        match &p.kind {
321            FormKind::Symbol(s) if s == "&" => {
322                saw_amp = true;
323            }
324            FormKind::Symbol(s) => {
325                if saw_amp {
326                    rest_param = Some(Arc::from(s.as_str()));
327                    rest_hint = hint;
328                    break;
329                } else {
330                    params.push(Arc::from(s.as_str()));
331                    param_hints.push(hint);
332                }
333            }
334            // Destructuring patterns: vectors and maps
335            FormKind::Vector(_) | FormKind::Map(_) => {
336                if saw_amp {
337                    let gensym = format!("__destructure_rest_{}", params.len());
338                    rest_param = Some(Arc::from(gensym.as_str()));
339                    destructure_rest = Some(p.clone());
340                    break;
341                } else {
342                    let idx = params.len();
343                    let gensym = format!("__destructure_{idx}");
344                    params.push(Arc::from(gensym.as_str()));
345                    // Destructured params can't be primitive-tagged.
346                    param_hints.push(None);
347                    destructure_params.push((idx, p.clone()));
348                }
349            }
350            _ => {
351                return Err(EvalError::Runtime(
352                    "fn params must be symbols, vectors, or maps".into(),
353                ));
354            }
355        }
356    }
357
358    let body = desugar_pre_post_conditions(body);
359
360    Ok(CljxFnArity {
361        params,
362        rest_param,
363        body,
364        destructure_params,
365        destructure_rest,
366        ir_arity_id: crate::interp::arity::fresh_arity_id(),
367        param_hints,
368        rest_hint,
369    })
370}
371
372/// Desugar a `:pre`/`:post` conditions map at the head of a function body.
373///
374/// Clojure binds `%` to the return value inside `:post` conditions. This
375/// transforms the raw body forms into equivalent assertion code so the
376/// interpreter does not need to handle conditions separately at call time.
377///
378/// Input body (simplified):
379/// ```text
380/// [{:pre [(pos? x)] :post [(pos? %)]} (inc x)]
381/// ```
382/// Output body:
383/// ```text
384/// [(assert (pos? x))
385///  (let* [% (inc x)]
386///    (assert (pos? %))
387///    %)]
388/// ```
389fn desugar_pre_post_conditions(body: &[Form]) -> Vec<Form> {
390    let first = match body.first() {
391        Some(f) => f,
392        None => return body.to_vec(),
393    };
394
395    let entries = match first.as_map() {
396        Some(entries) => entries,
397        None => return body.to_vec(),
398    };
399
400    let mut pre_conds: Vec<Form> = Vec::new();
401    let mut post_conds: Vec<Form> = Vec::new();
402    let mut has_conditions = false;
403
404    for chunk in entries.chunks(2) {
405        if chunk.len() < 2 {
406            break;
407        }
408        let (key, val) = (&chunk[0], &chunk[1]);
409        match &key.kind {
410            FormKind::Keyword(k) if k == "pre" => {
411                if let FormKind::Vector(conds) = &val.kind {
412                    pre_conds.extend_from_slice(conds);
413                    has_conditions = true;
414                }
415            }
416            FormKind::Keyword(k) if k == "post" => {
417                if let FormKind::Vector(conds) = &val.kind {
418                    post_conds.extend_from_slice(conds);
419                    has_conditions = true;
420                }
421            }
422            _ => {}
423        }
424    }
425
426    if !has_conditions {
427        return body.to_vec();
428    }
429
430    let real_body = &body[1..]; // strip the conditions map
431    let span = first.span.clone();
432    let mut new_body: Vec<Form> = Vec::new();
433
434    // Emit (assert cond) for each :pre condition.
435    for cond in &pre_conds {
436        new_body.push(make_assert_call(cond, &span));
437    }
438
439    if post_conds.is_empty() {
440        new_body.extend_from_slice(real_body);
441    } else {
442        // Wrap real body in (let* [% <body>] (assert post-cond)... %)
443        // so that % refers to the return value inside :post conditions.
444        let percent = Form::new(FormKind::Symbol("%".to_string()), span.clone());
445
446        let body_expr = if real_body.len() == 1 {
447            real_body[0].clone()
448        } else {
449            let mut do_forms = vec![Form::new(FormKind::Symbol("do".to_string()), span.clone())];
450            do_forms.extend_from_slice(real_body);
451            Form::new(FormKind::List(do_forms), span.clone())
452        };
453
454        let binding_vec = Form::new(
455            FormKind::Vector(vec![percent.clone(), body_expr]),
456            span.clone(),
457        );
458
459        let mut let_forms = vec![
460            Form::new(FormKind::Symbol("let*".to_string()), span.clone()),
461            binding_vec,
462        ];
463        for cond in &post_conds {
464            let_forms.push(make_assert_call(cond, &span));
465        }
466        let_forms.push(percent);
467
468        new_body.push(Form::new(FormKind::List(let_forms), span));
469    }
470
471    new_body
472}
473
474/// Build `(assert <cond>)` as a Form node.
475fn make_assert_call(cond: &Form, span: &cljrs_types::span::Span) -> Form {
476    Form::new(
477        FormKind::List(vec![
478            Form::new(FormKind::Symbol("assert".to_string()), span.clone()),
479            cond.clone(),
480        ]),
481        span.clone(),
482    )
483}
484
485/// Peel a `^hint` wrapper off a parameter form, returning the resolved
486/// primitive [`TypeHint`] (if the tag is a recognized primitive) together with
487/// the unwrapped inner form.  A param with no metadata returns `(None, form)`.
488fn peel_param_hint(p: &Form) -> (Option<TypeHint>, &Form) {
489    if let FormKind::Meta(meta, inner) = &p.kind {
490        let hint = tag_name_of_meta(meta).and_then(|n| TypeHint::from_tag(&n));
491        // Recurse in case of stacked metadata; the innermost form is the param.
492        let (inner_hint, inner_form) = peel_param_hint(inner);
493        (hint.or(inner_hint), inner_form)
494    } else {
495        (None, p)
496    }
497}
498
499/// Extract the bare tag name from a `^meta` form: `^long` (symbol shorthand) or
500/// `^{:tag long}` (explicit map).  Strips any namespace from the tag symbol.
501fn tag_name_of_meta(meta: &Form) -> Option<Arc<str>> {
502    match &meta.kind {
503        // `^long x` — the metadata is a bare symbol naming the tag.
504        FormKind::Symbol(s) => Some(strip_tag_ns(s)),
505        // `^{:tag long} x` — pull the `:tag` entry out of the map literal.
506        FormKind::Map(entries) => entries.chunks(2).find_map(|kv| {
507            let is_tag = matches!(&kv[0].kind, FormKind::Keyword(k) if k == "tag");
508            if !is_tag {
509                return None;
510            }
511            match kv.get(1).map(|f| &f.kind) {
512                Some(FormKind::Symbol(s)) => Some(strip_tag_ns(s)),
513                Some(FormKind::Str(s)) => Some(strip_tag_ns(s)),
514                _ => None,
515            }
516        }),
517        _ => None,
518    }
519}
520
521/// Strip a namespace prefix from a tag name (`clojure.core/long` → `long`).
522fn strip_tag_ns(s: &str) -> Arc<str> {
523    match s.rfind('/') {
524        Some(pos) if pos + 1 < s.len() => Arc::from(&s[pos + 1..]),
525        _ => Arc::from(s),
526    }
527}
528
529// ── if ────────────────────────────────────────────────────────────────────────
530
531fn eval_if(args: &[Form], env: &mut Env) -> EvalResult {
532    if args.is_empty() {
533        return Err(EvalError::Runtime("if requires a test".into()));
534    }
535    let test = eval(&args[0], env)?;
536    let truthy = !matches!(test, Value::Nil | Value::Bool(false));
537    if truthy {
538        if args.len() > 1 {
539            eval(&args[1], env)
540        } else {
541            Ok(Value::Nil)
542        }
543    } else if args.len() > 2 {
544        eval(&args[2], env)
545    } else {
546        Ok(Value::Nil)
547    }
548}
549
550// ── let* ──────────────────────────────────────────────────────────────────────
551
552fn eval_let(args: &[Form], env: &mut Env) -> EvalResult {
553    let bindings = match args.first().and_then(|f| f.as_vector()) {
554        Some(v) => expand_pairs(v)
555            .map_err(|_| EvalError::Runtime("let* binding vector must have even length".into()))?
556            .into_owned(),
557        None => return Err(EvalError::Runtime("let* requires a binding vector".into())),
558    };
559
560    let body = &args[1..];
561
562    // Detect assoc/conj chains that can be virtualized.
563    let chains = crate::interp::virtualize::detect_let_chains(&bindings);
564    let virtualizable_chains = find_virtualizable_chains(&chains, &bindings, body);
565
566    env.with_frame(|env| {
567        let pairs: Vec<_> = bindings.chunks(2).collect();
568        let mut i = 0;
569        while i < pairs.len() {
570            // Check if this binding starts a virtualizable chain.
571            if let Some(chain) = virtualizable_chains.iter().find(|c| c.start == i) {
572                match eval_virtualized_chain(chain, &pairs, env) {
573                    Ok(()) => {
574                        i += chain.len;
575                        continue;
576                    }
577                    Err(e) => {
578                        return Err(e);
579                    }
580                }
581            }
582
583            // Normal evaluation.
584            let pair = pairs[i];
585            let val = eval(&pair[1], env)?;
586            bind_pattern(&pair[0], val, env)?;
587            i += 1;
588        }
589
590        eval_body(body, env)
591    })
592}
593
594/// Filter chains to only those that are safe to virtualize.
595///
596/// A chain is safe if no intermediate binding is used outside the chain
597/// (i.e., it's only used as the collection argument of the next step).
598fn find_virtualizable_chains<'a>(
599    chains: &'a [crate::interp::virtualize::LetChain],
600    bindings: &[Form],
601    body: &[Form],
602) -> Vec<&'a crate::interp::virtualize::LetChain> {
603    chains
604        .iter()
605        .filter(|chain| {
606            // Check that no intermediate (all except the last) is used in body
607            // or in other bindings outside the chain.
608            for j in chain.start..(chain.start + chain.len - 1) {
609                let name = match &bindings[j * 2].kind {
610                    FormKind::Symbol(s) => s.as_str(),
611                    _ => return false,
612                };
613                if crate::interp::virtualize::binding_used_in_body(name, body) {
614                    return false;
615                }
616                if crate::interp::virtualize::binding_used_in_other_bindings(
617                    name,
618                    bindings,
619                    chain.start,
620                    chain.len,
621                ) {
622                    return false;
623                }
624            }
625            true
626        })
627        .collect()
628}
629
630/// Evaluate an assoc/conj chain using transient operations.
631///
632/// Instead of creating N intermediate persistent collections, we:
633/// 1. Evaluate the root collection (first arg of the first assoc/conj)
634/// 2. Convert to transient
635/// 3. Apply each assoc!/conj! mutably
636/// 4. Convert back to persistent
637/// 5. Bind only the final name (and intermediate names point to intermediate
638///    transient values for correctness, though they shouldn't be used).
639fn eval_virtualized_chain(
640    chain: &crate::interp::virtualize::LetChain,
641    pairs: &[&[Form]],
642    env: &mut Env,
643) -> Result<(), EvalError> {
644    use crate::builtins::transients::{
645        builtin_assoc_bang, builtin_conj_bang, builtin_persistent_bang, builtin_transient,
646    };
647
648    // Step 1: Evaluate the root collection (first arg of the first call).
649    let first_pair = pairs[chain.start];
650    let first_expr_forms = match &first_pair[1].kind {
651        FormKind::List(forms) => forms,
652        _ => unreachable!("chain detection ensures this is a list"),
653    };
654    let root_collection = eval(&first_expr_forms[1], env)?;
655
656    // Step 2: Convert to transient.
657    let mut transient = match builtin_transient(std::slice::from_ref(&root_collection)) {
658        Ok(t) => t,
659        Err(_) => {
660            // Collection doesn't support transients (e.g., sorted map).
661            // Fall back to normal evaluation for the whole chain.
662            return eval_chain_normally(chain, pairs, env);
663        }
664    };
665
666    // Step 3: Apply each chain operation using transient mutation.
667    for j in 0..chain.len {
668        let pair_idx = chain.start + j;
669        let pair = pairs[pair_idx];
670        let expr_forms = match &pair[1].kind {
671            FormKind::List(forms) => forms,
672            _ => unreachable!(),
673        };
674
675        // Evaluate the non-collection arguments.
676        let mut args = vec![transient.clone()];
677        for arg_form in expr_forms.iter().skip(2) {
678            args.push(eval(arg_form, env)?);
679        }
680
681        // Apply the transient operation.
682        transient = match chain.ops[j] {
683            crate::interp::virtualize::ChainOpKind::Assoc => {
684                builtin_assoc_bang(&args).map_err(|e| EvalError::Runtime(e.to_string()))?
685            }
686            crate::interp::virtualize::ChainOpKind::Conj => {
687                builtin_conj_bang(&args).map_err(|e| EvalError::Runtime(e.to_string()))?
688            }
689        };
690
691        // Bind intermediate names to a placeholder (they shouldn't be used,
692        // but we need them in the env for structural correctness).
693        // Only the last binding gets the persistent result.
694        if j < chain.len - 1 {
695            let name = match &pair[0].kind {
696                FormKind::Symbol(s) => s.clone(),
697                _ => unreachable!(),
698            };
699            // Bind to nil as placeholder — intermediates are verified as unused.
700            env.bind(Arc::from(name.as_str()), Value::Nil);
701        }
702    }
703
704    // Step 4: Convert back to persistent.
705    let persistent =
706        builtin_persistent_bang(&[transient]).map_err(|e| EvalError::Runtime(e.to_string()))?;
707
708    // Step 5: Bind the final name.
709    let last_pair = pairs[chain.start + chain.len - 1];
710    bind_pattern(&last_pair[0], persistent, env)?;
711
712    Ok(())
713}
714
715/// Fallback: evaluate a chain using normal persistent operations.
716fn eval_chain_normally(
717    chain: &crate::interp::virtualize::LetChain,
718    pairs: &[&[Form]],
719    env: &mut Env,
720) -> Result<(), EvalError> {
721    for j in 0..chain.len {
722        let pair_idx = chain.start + j;
723        let pair = pairs[pair_idx];
724        let val = eval(&pair[1], env)?;
725        bind_pattern(&pair[0], val, env)?;
726    }
727    Ok(())
728}
729
730// ── loop* / recur ─────────────────────────────────────────────────────────────
731
732pub fn eval_loop(args: &[Form], env: &mut Env) -> EvalResult {
733    let bindings = match args.first().and_then(|f| f.as_vector()) {
734        Some(v) => expand_pairs(v)
735            .map_err(|_| EvalError::Runtime("loop* binding vector must have even length".into()))?
736            .into_owned(),
737        None => return Err(EvalError::Runtime("loop* requires a binding vector".into())),
738    };
739
740    let body = &args[1..];
741
742    // Separate pattern forms and initial values.
743    let patterns: Vec<Form> = bindings.iter().step_by(2).cloned().collect();
744    let mut current_vals: Vec<Value> = Vec::new();
745
746    // Evaluate initial values.
747    for i in (1..bindings.len()).step_by(2) {
748        current_vals.push(eval(&bindings[i], env)?);
749    }
750
751    loop {
752        // Root current_vals so they survive GC — they're not yet bound in env.
753        let _vals_root = crate::env::gc_roots::root_values(&current_vals);
754
755        // GC safepoint on every loop iteration so tight recur loops
756        // don't starve the collector.
757        crate::env::gc_roots::gc_safepoint(env)?;
758
759        // Under no-gc: push a fresh scratch region for this iteration.
760        // Intermediates allocated in the body land here and are freed
761        // after the iteration ends.
762        #[cfg(feature = "no-gc")]
763        let mut scratch = cljrs_gc::alloc_ctx::ScratchGuard::new();
764
765        // Under GC: scope this iteration's heap allocations in a fresh alloc
766        // frame.  Every value the body allocates is rooted (via ALLOC_ROOTS)
767        // only until this frame drops at the end of the iteration; the
768        // intermediates — and the previous iteration's now-dead recur values —
769        // then become collectable, instead of being pinned for the lifetime of
770        // the enclosing top-level form.  `result` (the return value or recur
771        // values) is moved out before the frame drops and is re-rooted at the
772        // top of the next iteration (`root_values`) or by the caller during
773        // return unwinding; no GC safepoint runs in the interval, exactly as
774        // the IR/JIT dispatch seam relies on (see `crate::tiered::apply`).
775        #[cfg(not(feature = "no-gc"))]
776        let _iter_frame = cljrs_gc::push_alloc_frame();
777
778        let result = env.with_frame(|env| {
779            for (pat, val) in patterns.iter().zip(current_vals.iter()) {
780                bind_pattern(pat, val.clone(), env)?;
781            }
782
783            // Under no-gc: pop scratch before tail expression so the return value
784            // or recur args are allocated in the enclosing scope's context.
785            #[cfg(not(feature = "no-gc"))]
786            let result = eval_body_recur(body, env);
787            #[cfg(feature = "no-gc")]
788            let result = eval_body_with_scratch_loop(body, &mut scratch, env);
789
790            result
791        });
792        // scratch / _iter_frame drop at the end of the iteration (after the
793        // match below), freeing this iteration's intermediates.
794
795        match result {
796            Ok(v) => return Ok(v),
797            Err(EvalError::Recur(new_vals)) => {
798                if new_vals.len() != patterns.len() {
799                    return Err(EvalError::Arity {
800                        name: "recur".into(),
801                        expected: patterns.len().to_string(),
802                        got: new_vals.len(),
803                    });
804                }
805                // new_vals were evaluated in the caller's context (scratch was
806                // popped before the tail form), so they survive the reset.
807                current_vals = new_vals;
808            }
809            Err(e) => return Err(e),
810        }
811    }
812}
813
814fn eval_recur(args: &[Form], env: &mut Env) -> EvalResult {
815    let vals: Vec<Value> = args
816        .iter()
817        .map(|f| eval(f, env))
818        .collect::<EvalResult<_>>()?;
819    Err(EvalError::Recur(vals))
820}
821
822/// Eval body forms, propagating Recur without catching it.
823pub fn eval_body_recur(body: &[Form], env: &mut Env) -> EvalResult {
824    let mut result = Value::Nil;
825    for form in body {
826        result = eval(form, env)?;
827    }
828    Ok(result)
829}
830
831/// Under `no-gc`: eval loop body with scratch-region semantics.
832///
833/// Evaluates all non-tail forms inside the scratch region, then pops the
834/// scratch before the tail (return/recur) expression so it lands in the
835/// caller's context.
836#[cfg(feature = "no-gc")]
837fn eval_body_with_scratch_loop(
838    body: &[Form],
839    scratch: &mut cljrs_gc::alloc_ctx::ScratchGuard,
840    env: &mut Env,
841) -> EvalResult {
842    if body.is_empty() {
843        scratch.pop_for_return();
844        return Ok(Value::Nil);
845    }
846    for form in &body[..body.len() - 1] {
847        eval(form, env)?;
848    }
849    scratch.pop_for_return();
850    eval(&body[body.len() - 1], env)
851}
852
853// ── quote ─────────────────────────────────────────────────────────────────────
854
855fn eval_quote(args: &[Form], env: &Env) -> EvalResult {
856    match args.first() {
857        // `::kw` and an auto-resolved map's symbol keys resolve against the
858        // reading namespace even under quote - the JVM resolves them at read
859        // time, before quote can see them.
860        Some(f) => form_to_value(&resolve_auto_forms(f, env)?),
861        None => Err(EvalError::Runtime("quote requires an argument".into())),
862    }
863}
864
865// ── var ───────────────────────────────────────────────────────────────────────
866
867fn eval_var(args: &[Form], env: &mut Env) -> EvalResult {
868    let sym = match args.first().map(|f| &f.kind) {
869        Some(FormKind::Symbol(s)) => s.clone(),
870        _ => return Err(EvalError::Runtime("var requires a symbol".into())),
871    };
872    let parsed = cljrs_value::Symbol::parse(&sym);
873    let ns: Arc<str> = env.resolve_ns_or_current(parsed.namespace.as_deref());
874    let name = parsed.name.as_ref();
875    env.globals
876        .lookup_var_in_ns(&ns, name)
877        .map(Value::Var)
878        .ok_or_else(|| EvalError::UnboundSymbol(sym))
879}
880
881// ── set! ──────────────────────────────────────────────────────────────────────
882
883fn eval_set_bang(args: &[Form], env: &mut Env) -> EvalResult {
884    let target = args
885        .first()
886        .ok_or_else(|| EvalError::Runtime("set! requires a target".into()))?;
887    let val = if args.len() > 1 {
888        eval(&args[1], env)?
889    } else {
890        Value::Nil
891    };
892    match &target.kind {
893        FormKind::Symbol(sym) => set_bang_symbol(sym, val, env),
894        _ => match set_bang_field_target(target) {
895            Some((field, inst_form)) => {
896                let inst = eval(inst_form, env)?;
897                set_type_instance_field(&inst, field, val)
898            }
899            None => Err(set_bang_target_error()),
900        },
901    }
902}
903
904/// `(set! sym val)` for an already-evaluated `val`: a mutable deftype field in
905/// scope, else the thread-local binding of the var `sym` names, else its root.
906pub fn set_bang_symbol(sym: &str, val: Value, env: &mut Env) -> EvalResult {
907    // A bare unqualified name inside a deftype method may be one of its
908    // mutable fields; only if not does it fall through to var logic.
909    if !sym.contains('/')
910        && let Some(v) = try_set_mutable_field(env, sym, &val)?
911    {
912        return Ok(v);
913    }
914    let parsed = cljrs_value::Symbol::parse(sym);
915    let ns = parsed.namespace.as_deref().unwrap_or(&env.current_ns);
916    let var = env
917        .globals
918        .lookup_var_in_ns(ns, &parsed.name)
919        .ok_or_else(|| EvalError::UnboundSymbol(sym.to_string()))?;
920    // Prefer updating the thread-local binding if one exists.
921    if !crate::env::dynamics::set_thread_local(&var, val.clone()) {
922        var.get().bind(val.clone());
923    }
924    Ok(val)
925}
926
927/// For a `(set! (.-field inst) v)` target, the field name and the `inst`
928/// form; `None` for any other non-symbol target.
929pub fn set_bang_field_target(target: &Form) -> Option<(&str, &Form)> {
930    match &target.kind {
931        FormKind::List(parts) if parts.len() == 2 => match &parts[0].kind {
932            FormKind::Symbol(op) if op.starts_with(".-") => Some((&op[2..], &parts[1])),
933            _ => None,
934        },
935        _ => None,
936    }
937}
938
939/// The error for a `set!` target that is neither a symbol nor `(.-field inst)`.
940pub fn set_bang_target_error() -> EvalError {
941    EvalError::Runtime("set! requires a symbol or (.-field inst) target".into())
942}
943
944/// If `sym` names a mutable field of the `__deftype_self__` instance in scope,
945/// update its cell AND refresh the in-scope local snapshot, returning the new
946/// value. `None` when `sym` is not such a field, so `set!` falls through to var
947/// logic.
948fn try_set_mutable_field(env: &mut Env, sym: &str, val: &Value) -> EvalResult<Option<Value>> {
949    let Some(Value::TypeInstance(ti)) = env.lookup_local_frames(DEFTYPE_SELF) else {
950        return Ok(None);
951    };
952    let Some(atom) = ti.get().mutable.clone() else {
953        return Ok(None);
954    };
955    let key = Value::keyword(cljrs_value::Keyword::simple(sym));
956    let Value::Map(map) = atom.get().deref() else {
957        return Ok(None);
958    };
959    if map.get(&key).is_none() {
960        return Ok(None);
961    }
962    atom.get().reset(Value::Map(map.assoc(key, val.clone())));
963    // Keep the method's local snapshot consistent for later reads.
964    env.bind(Arc::from(sym), val.clone());
965    Ok(Some(val.clone()))
966}
967
968/// Set a mutable field on an explicit instance: `(set! (.-field inst) v)`.
969pub fn set_type_instance_field(inst: &Value, field: &str, val: Value) -> EvalResult {
970    let Value::TypeInstance(ti) = inst else {
971        return Err(EvalError::Runtime(format!(
972            "set! (.-{field} …): target is not a type instance"
973        )));
974    };
975    let atom = ti.get().mutable.clone().ok_or_else(|| {
976        EvalError::Runtime(format!("set! (.-{field} …): type has no mutable fields"))
977    })?;
978    let key = Value::keyword(cljrs_value::Keyword::simple(field));
979    let map = match atom.get().deref() {
980        Value::Map(m) => m,
981        _ => MapValue::empty(),
982    };
983    if map.get(&key).is_none() {
984        return Err(EvalError::Runtime(format!(
985            "set!: {field} is not a mutable field"
986        )));
987    }
988    atom.get().reset(Value::Map(map.assoc(key, val.clone())));
989    Ok(val)
990}
991
992// ── throw ─────────────────────────────────────────────────────────────────────
993
994fn eval_throw(args: &[Form], env: &mut Env) -> EvalResult {
995    let val = match args.first() {
996        Some(f) => eval(f, env)?,
997        None => Value::Nil,
998    };
999    Err(throw_value(val))
1000}
1001
1002/// The error `(throw val)` raises for an already-evaluated `val`.
1003///
1004/// Wraps non-error values in an ExceptionInfo so try/catch always sees a
1005/// Value::Error and ex-message / ex-data work uniformly inside the handler.
1006pub fn throw_value(val: Value) -> EvalError {
1007    let val = match val {
1008        Value::Error(_) => val,
1009        other => {
1010            let msg = format!("{}", other);
1011            Value::Error(GcPtr::new(ExceptionInfo::new(
1012                ValueError::Other(msg.clone()),
1013                msg,
1014                None,
1015                None,
1016            )))
1017        }
1018    };
1019    EvalError::Thrown(val)
1020}
1021
1022// ── try ───────────────────────────────────────────────────────────────────────
1023
1024/// A catch clause: `(catch Type binding body...)`.
1025///
1026/// Public so the async evaluator (`cljrs-async`) can reuse `parse_try_args` to
1027/// build a yielding `try`/`catch`.
1028pub struct CatchClause<'a> {
1029    pub type_sym: &'a str,
1030    pub binding: &'a str,
1031    pub body: &'a [Form],
1032}
1033
1034/// Convert a non-Thrown EvalError into a `Value::Error` so it can be bound
1035/// inside a catch clause and inspected with `ex-message` / `ex-data`.
1036///
1037/// Public so the async evaluator can convert errors the same way `try` does.
1038pub fn eval_error_to_value(err: &EvalError) -> Value {
1039    let msg = err.to_string();
1040    Value::Error(GcPtr::new(ExceptionInfo::new(
1041        ValueError::Other(msg.clone()),
1042        msg,
1043        None,
1044        None,
1045    )))
1046}
1047
1048/// Test whether the type symbol on a `(catch <Type> e ...)` clause matches a
1049/// thrown value. Type names are matched by their last `.`-separated segment so
1050/// fully-qualified names like `java.lang.Exception` work as well as bare ones.
1051pub fn catch_type_matches(type_name: &str, val: &Value) -> bool {
1052    let short = type_name.rsplit('.').next().unwrap_or(type_name);
1053    match short {
1054        // Catch-all (matches any value, error or not — back-compat).
1055        // `:default` is the ClojureScript universal catch keyword; it arrives
1056        // here as the keyword's name ("default") via `parse_try_args`.
1057        "Object" | "Exception" | "Throwable" | "Error" | "default" => true,
1058        // ExceptionInfo only matches actual ex-info / Exception values.
1059        "ExceptionInfo" => matches!(val, Value::Error(_)),
1060        _ => false,
1061    }
1062}
1063
1064fn eval_try(args: &[Form], env: &mut Env) -> EvalResult {
1065    let (body, catches, fin_body) = parse_try_args(args);
1066
1067    let mut result = eval_body(body, env);
1068
1069    // Handle catch: never intercept Recur (loop trampoline signal).
1070    let err_opt = match std::mem::replace(&mut result, Ok(Value::Nil)) {
1071        Ok(v) => {
1072            result = Ok(v);
1073            None
1074        }
1075        Err(EvalError::Recur(args)) => {
1076            result = Err(EvalError::Recur(args));
1077            None
1078        }
1079        Err(EvalError::GasExhausted) => {
1080            result = Err(EvalError::GasExhausted);
1081            None
1082        }
1083        Err(EvalError::MemoryLimitExceeded(fault)) => {
1084            result = Err(EvalError::MemoryLimitExceeded(fault));
1085            None
1086        }
1087        Err(other) => Some(other),
1088    };
1089
1090    if let Some(err) = err_opt {
1091        let thrown_val = match err {
1092            EvalError::Thrown(v) => v,
1093            ref other => eval_error_to_value(other),
1094        };
1095        let mut handled = false;
1096        for c in &catches {
1097            if catch_type_matches(c.type_sym, &thrown_val) {
1098                result = env.with_frame(|env| {
1099                    env.bind(Arc::from(c.binding), thrown_val.clone());
1100                    eval_body(c.body, env)
1101                });
1102                handled = true;
1103                break;
1104            }
1105        }
1106        if !handled {
1107            // No matching catch — re-throw.
1108            result = Err(EvalError::Thrown(thrown_val));
1109        }
1110    }
1111
1112    // Always run finally.  Its normal (Ok) value is discarded — `try` returns
1113    // the body/catch result — but an exception thrown from `finally` supersedes
1114    // the pending result or exception (matching JVM/Clojure semantics).
1115    if !fin_body.is_empty() {
1116        eval_body(fin_body, env)?;
1117    }
1118
1119    result
1120}
1121
1122/// The class name or `:default` keyword naming what a `catch` clause catches.
1123fn catch_target(f: &Form) -> Option<&str> {
1124    f.as_symbol().or_else(|| f.as_keyword())
1125}
1126
1127/// Split try args into (body, catch clauses, finally body).
1128///
1129/// Public so the async evaluator can parse `try` forms identically.
1130pub fn parse_try_args(args: &[Form]) -> (&[Form], Vec<CatchClause<'_>>, &[Form]) {
1131    let mut body_end = args.len();
1132    let mut catches: Vec<CatchClause<'_>> = Vec::new();
1133    let mut fin_body: &[Form] = &[];
1134
1135    for (i, form) in args.iter().enumerate() {
1136        if let Some(parts) = form.as_list()
1137            && let Some(s) = parts.first().and_then(|f| f.as_symbol())
1138        {
1139            if s == "catch" {
1140                if i < body_end {
1141                    body_end = i;
1142                }
1143                // The catch type may be a symbol (`Throwable`, `java.lang.Exception`),
1144                // the ClojureScript `:default` catch-all keyword, or a reader
1145                // conditional `#?(:rust Exception :clj ...)` whose selected branch
1146                // resolves to one of those.
1147                let type_sym = match parts.get(1) {
1148                    Some(f) => match catch_target(f) {
1149                        Some(name) => name,
1150                        None => match &f.unmeta().kind {
1151                            FormKind::ReaderCond {
1152                                splicing: false,
1153                                clauses,
1154                            } => match select_reader_cond(clauses).and_then(catch_target) {
1155                                Some(name) => name,
1156                                None => continue,
1157                            },
1158                            _ => continue,
1159                        },
1160                    },
1161                    None => continue,
1162                };
1163                let binding = match parts.get(2).and_then(|f| f.as_symbol()) {
1164                    Some(s) => s,
1165                    None => continue,
1166                };
1167                catches.push(CatchClause {
1168                    type_sym,
1169                    binding,
1170                    body: &parts[3..],
1171                });
1172                continue;
1173            }
1174            if s == "finally" {
1175                if i < body_end {
1176                    body_end = i;
1177                }
1178                fin_body = &parts[1..];
1179                continue;
1180            }
1181        }
1182    }
1183
1184    (&args[..body_end], catches, fin_body)
1185}
1186
1187// ── defn ──────────────────────────────────────────────────────────────────────
1188
1189pub fn eval_defn(args: &[Form], env: &mut Env, private: bool) -> EvalResult {
1190    // The name may carry reader metadata, e.g. `(defn ^:async fetch ...)`.
1191    let name_form = args
1192        .first()
1193        .ok_or_else(|| EvalError::Runtime("defn requires a symbol name".into()))?;
1194    let (name_metas, name_sym) = name_form.peel_meta();
1195    let (name, mut is_async) = match &name_sym.kind {
1196        FormKind::Symbol(s) => (s.clone(), name_metas.iter().any(|m| m.requests_async())),
1197        _ => return Err(EvalError::Runtime("defn name must be a symbol".into())),
1198    };
1199    // Optional docstring and/or metadata map after the name.
1200    // Valid orderings: (defn name body...), (defn name "doc" body...),
1201    // (defn name {:meta ...} body...), (defn name "doc" {:meta ...} body...).
1202    let mut rest_start = 1;
1203    let mut docstring: Option<String> = None;
1204    if rest_start < args.len()
1205        && let Some(s) = args[rest_start].as_string()
1206    {
1207        docstring = Some(s.to_string());
1208        rest_start += 1;
1209    }
1210    let mut attr_meta: Option<Value> = None;
1211    if rest_start < args.len() && args[rest_start].as_map().is_some() {
1212        // An attr-map such as `{:async true}` can also request async dispatch.
1213        is_async |= args[rest_start].requests_async();
1214        attr_meta = Some(eval(&args[rest_start], env)?);
1215        rest_start += 1;
1216    }
1217    // Build (fn* name ...)
1218    let mut fn_args = vec![Form::new(
1219        FormKind::Symbol(name.to_string()),
1220        args[0].span.clone(),
1221    )];
1222    fn_args.extend_from_slice(&args[rest_start..]);
1223    // Under no-gc: the Fn object must live in the StaticArena since the Var
1224    // intern outlives all scratch regions.
1225    #[cfg(feature = "no-gc")]
1226    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1227    let mut fn_val = eval_fn(&fn_args, env)?;
1228    if is_async && let Value::Fn(ref mut f) = fn_val {
1229        f.get_mut().is_async = true;
1230    }
1231    let var = env
1232        .globals
1233        .intern(&env.current_ns, Arc::from(name.as_str()), fn_val.clone());
1234    let mut meta = attr_meta;
1235    if let Some(doc) = &docstring {
1236        meta = merge_meta(meta, Some(doc_meta(doc)));
1237    }
1238    meta = merge_meta(meta, arglists_meta(&fn_val, 0));
1239    if private {
1240        meta = merge_meta(meta, Some(private_meta()));
1241    }
1242    if let Some(meta_val) = meta {
1243        var.get().set_meta(meta_val);
1244    }
1245    Ok(Value::Var(var))
1246}
1247
1248// ── defmacro ──────────────────────────────────────────────────────────────────
1249
1250/// Prepend `&form` and `&env` Form symbols to an arity form's parameter vector.
1251///
1252/// Handles:
1253/// - Single-arity vector `[params...]` → `[&form &env params...]`
1254/// - Multi-arity clause list `([params...] body...)` → `([&form &env params...] body...)`
1255fn prepend_macro_params(form: &Form) -> Form {
1256    let span = form.span.clone();
1257    match &form.unmeta().kind {
1258        FormKind::Vector(params) => {
1259            let mut new_params = vec![
1260                Form::new(FormKind::Symbol("&form".to_string()), span.clone()),
1261                Form::new(FormKind::Symbol("&env".to_string()), span.clone()),
1262            ];
1263            new_params.extend_from_slice(params);
1264            Form::new(FormKind::Vector(new_params), span)
1265        }
1266        FormKind::List(forms) => {
1267            // Arity clause: ([params...] body...) — prepend to first element (params vector).
1268            if let Some(first) = forms.first() {
1269                let new_params_form = prepend_macro_params(first);
1270                let mut new_forms = vec![new_params_form];
1271                new_forms.extend_from_slice(&forms[1..]);
1272                Form::new(FormKind::List(new_forms), span)
1273            } else {
1274                form.clone()
1275            }
1276        }
1277        _ => form.clone(),
1278    }
1279}
1280
1281fn eval_defmacro(args: &[Form], env: &mut Env) -> EvalResult {
1282    // The name may carry reader metadata, e.g. `(defmacro ^:private foo ...)`,
1283    // possibly stacked (`^:a ^:b foo`).
1284    let mut current = args
1285        .first()
1286        .ok_or_else(|| EvalError::Runtime("defmacro requires a symbol at position 0".into()))?
1287        .clone();
1288    let mut name_meta: Option<Value> = None;
1289    while let FormKind::Meta(meta_form, inner) = current.kind {
1290        let meta_val = compile_meta_form(&meta_form, env)?;
1291        name_meta = merge_meta(name_meta, Some(meta_val));
1292        current = *inner;
1293    }
1294    let name = match &current.kind {
1295        FormKind::Symbol(s) => s.clone(),
1296        _ => {
1297            return Err(EvalError::Runtime(
1298                "defmacro requires a symbol at position 0".into(),
1299            ));
1300        }
1301    };
1302
1303    let mut rest_start = 1;
1304    let mut docstring: Option<String> = None;
1305    if rest_start < args.len()
1306        && let FormKind::Str(s) = &args[rest_start].kind
1307    {
1308        docstring = Some(s.clone());
1309        rest_start += 1;
1310    }
1311    let mut attr_meta = None;
1312    if rest_start < args.len() && matches!(args[rest_start].kind, FormKind::Map(_)) {
1313        attr_meta = Some(eval(&args[rest_start], env)?);
1314        rest_start += 1;
1315    }
1316    // Prepend implicit &form and &env params to each arity.
1317    let mut fn_args = vec![Form::new(
1318        FormKind::Symbol(name.to_string()),
1319        args[0].span.clone(),
1320    )];
1321    for form in &args[rest_start..] {
1322        fn_args.push(prepend_macro_params(form));
1323    }
1324    // Under no-gc: the Macro object must live in the StaticArena since the Var
1325    // intern outlives all scratch regions.
1326    #[cfg(feature = "no-gc")]
1327    let _static_ctx = cljrs_gc::alloc_ctx::StaticCtxGuard::new();
1328    let fn_val = eval_fn(&fn_args, env)?;
1329
1330    // Convert Fn → Macro by setting is_macro = true.
1331    let macro_val = match fn_val {
1332        Value::Fn(f) => {
1333            let mut mfn = f.get().clone();
1334            mfn.is_macro = true;
1335            Value::Macro(GcPtr::new(mfn))
1336        }
1337        other => other,
1338    };
1339
1340    let var = env
1341        .globals
1342        .intern(&env.current_ns, Arc::from(name.as_str()), macro_val.clone());
1343    let mut meta = merge_meta(name_meta, attr_meta);
1344    if let Some(doc) = &docstring {
1345        meta = merge_meta(meta, Some(doc_meta(doc)));
1346    }
1347    // Skip the implicit &form/&env params when showing the macro's signature.
1348    meta = merge_meta(meta, arglists_meta(&macro_val, 2));
1349    if let Some(m) = meta {
1350        var.get().set_meta(m);
1351    }
1352    Ok(Value::Var(var))
1353}
1354
1355// ── defonce ───────────────────────────────────────────────────────────────────
1356
1357fn eval_defonce(args: &[Form], env: &mut Env) -> EvalResult {
1358    if args.is_empty() {
1359        return Err(EvalError::Runtime("defonce requires a name".into()));
1360    }
1361    // Share `def`'s name extraction so a metadata-carrying symbol such as
1362    // `(defonce ^:private registry (atom {}))` is accepted here too.
1363    let (name, _meta) = extract_def_name(&args[0], env)?;
1364    // If already bound, return immediately.
1365    if let Some(var) = defonce_existing(&name, env) {
1366        return Ok(var);
1367    }
1368    eval_def(args, env)
1369}
1370
1371// ── and / or ──────────────────────────────────────────────────────────────────
1372
1373fn eval_and(args: &[Form], env: &mut Env) -> EvalResult {
1374    let mut result = Value::Bool(true);
1375    for form in args {
1376        result = eval(form, env)?;
1377        if matches!(result, Value::Nil | Value::Bool(false)) {
1378            return Ok(result);
1379        }
1380    }
1381    Ok(result)
1382}
1383
1384fn eval_or(args: &[Form], env: &mut Env) -> EvalResult {
1385    let mut last = Value::Nil;
1386    for form in args {
1387        last = eval(form, env)?;
1388        if !matches!(last, Value::Nil | Value::Bool(false)) {
1389            return Ok(last);
1390        }
1391    }
1392    Ok(last)
1393}
1394
1395// ── require ───────────────────────────────────────────────────────────────────
1396
1397fn eval_require(args: &[Form], env: &mut Env) -> EvalResult {
1398    for arg in args {
1399        let val = eval(arg, env)?;
1400        let spec = parse_require_spec_val(val).map_err(EvalError::Runtime)?;
1401        load_ns(env.globals.clone(), &spec, &env.current_ns)?;
1402    }
1403    Ok(Value::Nil)
1404}
1405
1406/// Parse a `RequireSpec` from an already-evaluated `Value`.
1407/// Accepts: `'some.ns`, `['some.ns :as alias]`, `['some.ns :refer [syms]]`,
1408/// `['some.ns :refer :all]`, and versioned forms like `'some.ns@abc1234` or
1409/// `['some.ns@abc1234 :as alias]`.
1410fn parse_require_spec_val(val: Value) -> Result<RequireSpec, String> {
1411    match val {
1412        Value::Symbol(s) => {
1413            let sym = s.get();
1414            Ok(RequireSpec {
1415                ns: sym.name.clone(),
1416                version: sym.version.clone(),
1417                alias: None,
1418                refer: RequireRefer::None,
1419            })
1420        }
1421        Value::Vector(v) => {
1422            let items: Vec<Value> = v.get().iter().cloned().collect();
1423            if items.is_empty() {
1424                return Err("require spec vector must not be empty".into());
1425            }
1426            let (ns, version) = match &items[0] {
1427                Value::Symbol(s) => {
1428                    let sym = s.get();
1429                    (sym.name.clone(), sym.version.clone())
1430                }
1431                other => {
1432                    return Err(format!(
1433                        "require spec: first element must be a symbol, got {}",
1434                        other.type_name()
1435                    ));
1436                }
1437            };
1438            let mut alias = None;
1439            let mut refer = RequireRefer::None;
1440            let mut i = 1;
1441            while i < items.len() {
1442                match &items[i] {
1443                    Value::Keyword(k) if k.get().name.as_ref() == "as" => {
1444                        i += 1;
1445                        alias = Some(match items.get(i) {
1446                            Some(Value::Symbol(s)) => s.get().name.clone(),
1447                            _ => return Err("require :as expects a symbol".into()),
1448                        });
1449                    }
1450                    Value::Keyword(k) if k.get().name.as_ref() == "refer" => {
1451                        i += 1;
1452                        refer = match items.get(i) {
1453                            Some(Value::Keyword(k2)) if k2.get().name.as_ref() == "all" => {
1454                                RequireRefer::All
1455                            }
1456                            Some(Value::Vector(rv)) => {
1457                                let names: Vec<Arc<str>> = rv
1458                                    .get()
1459                                    .iter()
1460                                    .map(|v| match v {
1461                                        Value::Symbol(s) => Ok(s.get().name.clone()),
1462                                        other => Err(format!(
1463                                            "require :refer expects symbols, got {}",
1464                                            other.type_name()
1465                                        )),
1466                                    })
1467                                    .collect::<Result<_, _>>()?;
1468                                RequireRefer::Named(names)
1469                            }
1470                            _ => {
1471                                return Err(
1472                                    "require :refer expects :all or a vector of symbols".into()
1473                                );
1474                            }
1475                        };
1476                    }
1477                    other => {
1478                        return Err(format!(
1479                            "require spec: unexpected option {}",
1480                            other.type_name()
1481                        ));
1482                    }
1483                }
1484                i += 1;
1485            }
1486            Ok(RequireSpec {
1487                ns,
1488                version,
1489                alias,
1490                refer,
1491            })
1492        }
1493        other => Err(format!(
1494            "require expects a symbol or vector, got {}",
1495            other.type_name()
1496        )),
1497    }
1498}
1499
1500/// The form a require-spec element denotes: a reader conditional resolves to
1501/// its selected branch, `None` when no branch matches this platform. Any other
1502/// form is itself.
1503fn spec_element(form: &Form) -> Option<&Form> {
1504    match &form.kind {
1505        FormKind::ReaderCond { clauses, .. } => select_reader_cond(clauses),
1506        _ => Some(form),
1507    }
1508}
1509
1510/// Parse a `RequireSpec` from a raw `Form` (unevaluated, used in `ns` macro).
1511/// Also handles versioned namespace symbols such as `my.ns@abc1234`.
1512///
1513/// Public so the AOT compiler can establish an entry namespace from the same
1514/// parse `eval_ns` uses. A second implementation there would drift: it already
1515/// did, dropping `:refer` and `@version` from structurally emitted requires.
1516pub fn parse_require_spec_form(form: &Form) -> Result<RequireSpec, String> {
1517    match &form.kind {
1518        FormKind::Symbol(s) => {
1519            let sym = cljrs_value::Symbol::parse(s);
1520            Ok(RequireSpec {
1521                ns: sym.name.clone(),
1522                version: sym.version.clone(),
1523                alias: None,
1524                refer: RequireRefer::None,
1525            })
1526        }
1527        FormKind::Vector(items) => {
1528            if items.is_empty() {
1529                return Err("require spec vector must not be empty".into());
1530            }
1531            let head = spec_element(&items[0]).ok_or_else(|| {
1532                "require spec: no reader-conditional branch matched for the namespace".to_string()
1533            })?;
1534            let (ns, version) = match &head.kind {
1535                FormKind::Symbol(s) => {
1536                    let sym = cljrs_value::Symbol::parse(s);
1537                    (sym.name.clone(), sym.version.clone())
1538                }
1539                _ => return Err("require spec: first element must be a symbol".into()),
1540            };
1541            let mut alias = None;
1542            let mut refer = RequireRefer::None;
1543            let mut i = 1;
1544            while i < items.len() {
1545                // An option whose conditional selects nothing is dropped; the
1546                // namespace slot above cannot be, so it errors instead.
1547                let Some(item) = spec_element(&items[i]) else {
1548                    i += 1;
1549                    continue;
1550                };
1551                match &item.kind {
1552                    FormKind::Keyword(k) if k == "as" => {
1553                        i += 1;
1554                        alias = Some(match items.get(i).map(|f| &f.kind) {
1555                            Some(FormKind::Symbol(s)) => Arc::from(s.as_str()),
1556                            _ => return Err("require :as expects a symbol".into()),
1557                        });
1558                    }
1559                    FormKind::Keyword(k) if k == "refer" => {
1560                        i += 1;
1561                        refer = match items.get(i).map(|f| &f.kind) {
1562                            Some(FormKind::Keyword(k2)) if k2 == "all" => RequireRefer::All,
1563                            Some(FormKind::Vector(rv)) => {
1564                                let names: Vec<Arc<str>> = rv
1565                                    .iter()
1566                                    .map(|f| match &f.kind {
1567                                        FormKind::Symbol(s) => Ok(Arc::from(s.as_str())),
1568                                        _ => Err("require :refer expects symbols".to_string()),
1569                                    })
1570                                    .collect::<Result<_, _>>()?;
1571                                RequireRefer::Named(names)
1572                            }
1573                            _ => return Err("require :refer expects :all or a vector".into()),
1574                        };
1575                    }
1576                    _ => return Err(format!("require spec: unexpected form at position {i}")),
1577                }
1578                i += 1;
1579            }
1580            Ok(RequireSpec {
1581                ns,
1582                version,
1583                alias,
1584                refer,
1585            })
1586        }
1587        _ => Err("require spec must be a symbol or vector".into()),
1588    }
1589}
1590
1591// ── ns ────────────────────────────────────────────────────────────────────────
1592
1593/// Extract the ns name and optional metadata from the `ns` macro's name form.
1594/// Handles plain symbols and `^meta` shorthand / map forms, e.g.
1595/// `(ns ^{:doc "..."} my.ns ...)` or `(ns ^:no-doc my.ns ...)`.
1596fn extract_ns_name_form(form: Option<&Form>, env: &mut Env) -> EvalResult<(String, Option<Value>)> {
1597    let mut current = form
1598        .ok_or_else(|| EvalError::Runtime("ns requires a symbol at position 0".into()))?
1599        .clone();
1600    let mut meta_acc: Option<Value> = None;
1601    while let FormKind::Meta(meta_form, inner) = current.kind {
1602        let meta_val = compile_meta_form(&meta_form, env)?;
1603        meta_acc = merge_meta(meta_acc, Some(meta_val));
1604        current = *inner;
1605    }
1606    match &current.kind {
1607        FormKind::Symbol(s) => Ok((s.clone(), meta_acc)),
1608        _ => Err(EvalError::Runtime(
1609            "ns requires a symbol at position 0".into(),
1610        )),
1611    }
1612}
1613
1614/// Merge two optional metadata maps, with `overlay` entries taking
1615/// precedence over `base` entries (matching Clojure's `(merge base overlay)`).
1616fn merge_meta(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
1617    match (base, overlay) {
1618        (None, None) => None,
1619        (Some(b), None) => Some(b),
1620        (None, Some(o)) => Some(o),
1621        (Some(Value::Map(b)), Some(Value::Map(o))) => {
1622            let mut merged = b;
1623            for (k, v) in o.iter() {
1624                merged = merged.assoc(k.clone(), v.clone());
1625            }
1626            Some(Value::Map(merged))
1627        }
1628        (_, Some(o)) => Some(o),
1629    }
1630}
1631
1632/// Parse the body of a `(:refer-clojure ...)` clause into the filter applied
1633/// to the automatic `clojure.core` refer.  Accepts `:exclude`, `:only` and
1634/// `:rename`, each once, in any order.
1635fn parse_refer_clojure_clause(items: &[Form]) -> Result<ReferClojureFilter, String> {
1636    let mut filter = ReferClojureFilter::default();
1637    let items = expand_reader_conds(items);
1638    let mut i = 0;
1639    while i < items.len() {
1640        let FormKind::Keyword(k) = &items[i].kind else {
1641            return Err(":refer-clojure expects keyword options (:exclude, :only, :rename)".into());
1642        };
1643        let arg = items
1644            .get(i + 1)
1645            .ok_or_else(|| format!(":refer-clojure :{k} expects a value"))?;
1646        match k.as_str() {
1647            "exclude" => filter.exclude = symbol_name_set(arg, "exclude")?,
1648            "only" => filter.only = Some(symbol_name_set(arg, "only")?),
1649            "rename" => {
1650                let FormKind::Map(entries) = &arg.kind else {
1651                    return Err(":refer-clojure :rename expects a map".into());
1652                };
1653                if entries.len() % 2 != 0 {
1654                    return Err(":refer-clojure :rename expects an even-sized map".into());
1655                }
1656                for pair in entries.chunks(2) {
1657                    match (&pair[0].kind, &pair[1].kind) {
1658                        (FormKind::Symbol(from), FormKind::Symbol(to)) => {
1659                            filter
1660                                .rename
1661                                .insert(Arc::from(from.as_str()), Arc::from(to.as_str()));
1662                        }
1663                        _ => {
1664                            return Err(
1665                                ":refer-clojure :rename expects symbol keys and values".into()
1666                            );
1667                        }
1668                    }
1669                }
1670            }
1671            other => return Err(format!(":refer-clojure does not support :{other}")),
1672        }
1673        i += 2;
1674    }
1675    Ok(filter)
1676}
1677
1678/// Read a vector of symbols (an `:exclude`/`:only` list) as a set of names.
1679fn symbol_name_set(form: &Form, opt: &str) -> Result<HashSet<Arc<str>>, String> {
1680    let FormKind::Vector(items) = &form.kind else {
1681        return Err(format!(":refer-clojure :{opt} expects a vector of symbols"));
1682    };
1683    expand_reader_conds(items)
1684        .iter()
1685        .map(|f| match &f.kind {
1686            FormKind::Symbol(sym) => Ok(Arc::from(sym.as_str())),
1687            _ => Err(format!(":refer-clojure :{opt} expects symbols")),
1688        })
1689        .collect()
1690}
1691
1692fn eval_ns(args: &[Form], env: &mut Env) -> EvalResult {
1693    let (name, name_meta) = extract_ns_name_form(args.first(), env)?;
1694    env.globals.get_or_create_ns(&name);
1695    env.current_ns = Arc::from(name.as_str());
1696    // Auto-refer clojure.core (Clojure default behaviour).
1697    if name != "clojure.core" {
1698        env.globals.refer_core(&name);
1699    }
1700    sync_star_ns(env);
1701
1702    // Optional docstring, then optional attr-map, before reference clauses.
1703    let mut rest = &args[1..];
1704    if matches!(rest.first().map(|f| &f.kind), Some(FormKind::Str(_))) {
1705        rest = &rest[1..];
1706    }
1707    let mut attr_meta = None;
1708    if matches!(rest.first().map(|f| &f.kind), Some(FormKind::Map(_))) {
1709        attr_meta = Some(eval(&rest[0], env)?);
1710        rest = &rest[1..];
1711    }
1712
1713    if let Some(m) = merge_meta(name_meta, attr_meta) {
1714        let ns_ptr = env.globals.get_or_create_ns(&env.current_ns);
1715        ns_ptr.get().set_meta(m);
1716    }
1717
1718    // `:refer-clojure` first: it narrows the automatic core refer done above,
1719    // and an explicit `:require ... :refer` below must be able to override it.
1720    // Re-evaluating an `ns` form without the clause clears a filter left by an
1721    // earlier evaluation, restoring the full core refer.
1722    //
1723    // More than one `:refer-clojure` clause is not meaningful, so the last one
1724    // wins rather than accumulating.  (Clojure runs each clause as a separate
1725    // `refer` call, where a second clause silently *re-adds* what the first
1726    // excluded; there is no reading of that under which both clauses do what
1727    // they say.)
1728    let mut refer_clojure = None;
1729    for clause in rest {
1730        if let FormKind::List(items) = &clause.kind
1731            && matches!(items.first().map(|f| &f.kind), Some(FormKind::Keyword(k)) if k == "refer-clojure")
1732        {
1733            refer_clojure =
1734                Some(parse_refer_clojure_clause(&items[1..]).map_err(EvalError::Runtime)?);
1735        }
1736    }
1737    if name != "clojure.core" {
1738        env.globals
1739            .set_refer_clojure_filter(&name, refer_clojure)
1740            .map_err(EvalError::Runtime)?;
1741    }
1742
1743    for clause in rest {
1744        if let FormKind::List(items) = &clause.kind {
1745            match items.first().map(|f| &f.kind) {
1746                Some(FormKind::Keyword(k)) if k == "require" => {
1747                    // Expand reader conditionals among require specs
1748                    let expanded = expand_reader_conds(&items[1..]);
1749                    for spec_form in &expanded {
1750                        let spec =
1751                            parse_require_spec_form(spec_form).map_err(EvalError::Runtime)?;
1752                        load_ns(env.globals.clone(), &spec, &name)?;
1753                    }
1754                }
1755                Some(FormKind::Keyword(k)) if k == "import" => {
1756                    for spec in &expand_reader_conds(&items[1..]) {
1757                        import_types(env, &name, spec);
1758                    }
1759                }
1760                // `:refer-clojure` was handled in the pass above; other clauses
1761                // (`:use`) — skip for now.
1762                _ => {}
1763            }
1764        }
1765    }
1766
1767    let ns_ptr = env.globals.get_or_create_ns(&env.current_ns);
1768    Ok(Value::Namespace(ns_ptr))
1769}
1770
1771/// Apply one `(:import ...)` spec (`[pkg Name ...]`, `(pkg Name ...)` or
1772/// `pkg.Name`) by referring each named record or deftype into `dst_ns`.
1773///
1774/// A type's dispatch tag is qualified by its defining namespace, so an
1775/// unqualified `Name` only reaches that tag through a var `dst_ns` can
1776/// resolve; without the refer, `(extend-type Name ...)` in the importing
1777/// namespace registers its methods under a tag no instance carries.
1778fn import_types(env: &Env, dst_ns: &str, spec: &Form) {
1779    match &spec.unmeta().kind {
1780        FormKind::Vector(items) | FormKind::List(items) => {
1781            let mut syms = items.iter().filter_map(Form::as_symbol);
1782            if let Some(pkg) = syms.next() {
1783                for name in syms {
1784                    import_type(env, dst_ns, pkg, name);
1785                }
1786            }
1787        }
1788        FormKind::Symbol(s) => {
1789            if let Some((pkg, name)) = s.rsplit_once('.') {
1790                import_type(env, dst_ns, pkg, name);
1791            }
1792        }
1793        _ => {}
1794    }
1795}
1796
1797/// Refer the type `name` defined in namespace `pkg` into `dst_ns`. `pkg` is
1798/// also tried with `_` read as `-`, the JVM's package spelling of a namespace.
1799/// Anything that is not a loaded record or deftype (a host class in a `.cljc`
1800/// file, say) is left alone, as `:import` was before it referred anything.
1801fn import_type(env: &Env, dst_ns: &str, pkg: &str, name: &str) {
1802    let demunged = pkg.replace('_', "-");
1803    for ns in [pkg, demunged.as_str()] {
1804        let tag = format!("{ns}.{name}");
1805        let names_type = matches!(
1806            env.globals.lookup_in_ns(ns, name),
1807            Some(Value::Symbol(s)) if s.get().to_string() == tag
1808        );
1809        if names_type {
1810            env.globals.refer_named(dst_ns, ns, &[Arc::from(name)]);
1811            return;
1812        }
1813    }
1814}
1815
1816// ── load-file ─────────────────────────────────────────────────────────────────
1817
1818fn eval_load_file(args: &[Form], env: &mut Env) -> EvalResult {
1819    if args.is_empty() {
1820        return Err(EvalError::Runtime(
1821            "load-file requires a path argument".into(),
1822        ));
1823    }
1824    let path_val = eval(&args[0], env)?;
1825    let path = match &path_val {
1826        Value::Str(s) => s.get().clone(),
1827        v => {
1828            return Err(EvalError::Runtime(format!(
1829                "load-file: expected string, got {}",
1830                v.type_name()
1831            )));
1832        }
1833    };
1834    let src = std::fs::read_to_string(&path)
1835        .map_err(|e| EvalError::Runtime(format!("load-file: {e}")))?;
1836    let mut parser = cljrs_reader::Parser::new(src, path.clone());
1837    let forms = parser
1838        .parse_all()
1839        .map_err(|e| EvalError::Runtime(format!("load-file parse error: {e}")))?;
1840    let mut result = Value::Nil;
1841    for form in forms {
1842        let _alloc_frame = cljrs_gc::push_alloc_frame();
1843        result = eval(&form, env)?;
1844    }
1845    Ok(result)
1846}
1847
1848// ── letfn ─────────────────────────────────────────────────────────────────────
1849
1850fn eval_letfn(args: &[Form], env: &mut Env) -> EvalResult {
1851    push_letfn_frame(args, env)?;
1852    let result = eval_body(&args[1..], env);
1853    env.pop_frame();
1854    result
1855}
1856
1857/// Push a local frame binding every fn of `(letfn [fns…] body…)`, mutually
1858/// visible. On success the caller evaluates the body and pops the frame; on
1859/// error the frame has already been popped.
1860///
1861/// Public so the async evaluator can run the body with a yielding evaluator.
1862pub fn push_letfn_frame(args: &[Form], env: &mut Env) -> EvalResult<()> {
1863    // (letfn [(f [params] body...) ...] body...)
1864    //
1865    // Three passes, because a closure here captures VALUES, not cells:
1866    // `eval_fn` snapshots `env.all_local_bindings()`, so a name bound after the
1867    // closure was built is invisible to it forever. Binding each fn as it was
1868    // built therefore gave letfn `let`-like sequential scope — a backward
1869    // reference resolved, a forward one or a mutual pair raised "unbound
1870    // symbol", which defeats the only reason letfn exists.
1871    let bindings = match args.first().and_then(|f| f.as_vector()) {
1872        Some(v) => expand_reader_conds_cow(v).into_owned(),
1873        None => return Err(EvalError::Runtime("letfn requires a binding vector".into())),
1874    };
1875
1876    env.push_frame();
1877
1878    // Pass 1: bind every name to nil, so the pass-2 snapshot CONTAINS all of
1879    // them. A capture list cannot grow after the fact; it can only be corrected.
1880    for binding in &bindings {
1881        if let Some(parts) = binding.as_list() {
1882            if parts.is_empty() {
1883                continue;
1884            }
1885            let Some(name) = parts[0].as_symbol() else {
1886                env.pop_frame();
1887                return Err(EvalError::Runtime(
1888                    "letfn binding name must be a symbol".into(),
1889                ));
1890            };
1891            env.bind(Arc::from(name), Value::Nil);
1892        }
1893    }
1894
1895    // Pass 2: build the closures. Each captures the real value of any sibling
1896    // already built and nil for the rest.
1897    let mut built: Vec<(Arc<str>, Value)> = Vec::new();
1898    for binding in &bindings {
1899        if let Some(parts) = binding.as_list() {
1900            if parts.is_empty() {
1901                continue;
1902            }
1903            // parts[0] is the name, so eval_fn sees this as a NAMED fn and wires
1904            // up its self-reference; pass 3 handles every other direction.
1905            let fn_val = match eval_fn(parts, env) {
1906                Ok(v) => v,
1907                Err(e) => {
1908                    env.pop_frame();
1909                    return Err(e);
1910                }
1911            };
1912            let name: Arc<str> = match parts[0].as_symbol() {
1913                Some(s) => Arc::from(s),
1914                None => unreachable!("pass 1 rejected every non-symbol name"),
1915            };
1916            env.bind(Arc::clone(&name), fn_val.clone());
1917            built.push((name, fn_val));
1918        }
1919    }
1920
1921    // Pass 3: replace the nil placeholders each closure captured with the
1922    // sibling it actually names. This is what makes the scope MUTUAL rather
1923    // than sequential, and it necessarily builds a reference cycle between
1924    // co-recursive fns — which is inherent to letfn, not an artifact here.
1925    for (_, fn_val) in &built {
1926        if let Value::Fn(ptr) = fn_val {
1927            let mut ptr = ptr.clone();
1928            let f = ptr.get_mut();
1929            for i in 0..f.closed_over_names.len() {
1930                let captured = f.closed_over_names[i].clone();
1931                if let Some((_, real)) = built.iter().find(|(n, _)| *n == captured) {
1932                    f.closed_over_vals[i] = real.clone();
1933                }
1934            }
1935        }
1936    }
1937
1938    Ok(())
1939}
1940
1941// ── in-ns ─────────────────────────────────────────────────────────────────────
1942
1943fn eval_in_ns(args: &[Form], env: &mut Env) -> EvalResult {
1944    // (in-ns 'foo.bar)
1945    if args.is_empty() {
1946        return Err(EvalError::Runtime("in-ns requires a namespace name".into()));
1947    }
1948    let ns_val = eval(&args[0], env)?;
1949    let ns_name = extract_ns_name(&ns_val)?;
1950    env.globals.get_or_create_ns(&ns_name);
1951    env.globals.refer_core(&ns_name);
1952    env.current_ns = Arc::from(ns_name.as_str());
1953    sync_star_ns(env);
1954    let ns_ptr = env.globals.get_or_create_ns(&env.current_ns);
1955    Ok(Value::Namespace(ns_ptr))
1956}
1957
1958// ── alias ─────────────────────────────────────────────────────────────────────
1959
1960fn eval_alias(args: &[Form], env: &mut Env) -> EvalResult {
1961    // (alias 'short 'some.long.ns)
1962    if args.len() < 2 {
1963        return Err(EvalError::Runtime(
1964            "alias requires alias-sym and namespace-sym".into(),
1965        ));
1966    }
1967    let alias_val = eval(&args[0], env)?;
1968    let ns_val = eval(&args[1], env)?;
1969
1970    let alias_name = extract_ns_name(&alias_val)?;
1971    let full_ns = extract_ns_name(&ns_val)?;
1972
1973    let ns_ptr = env.globals.get_or_create_ns(&env.current_ns);
1974    let mut aliases = ns_ptr.get().aliases.lock().unwrap();
1975    aliases.insert(Arc::from(alias_name.as_str()), Arc::from(full_ns.as_str()));
1976    Ok(Value::Nil)
1977}
1978
1979/// Extract a namespace-name string from a Value::Symbol, Value::Str, or Value::Keyword.
1980fn extract_ns_name(v: &Value) -> EvalResult<String> {
1981    match v {
1982        Value::Symbol(s) => {
1983            // Use the full name (e.g. "clojure.core").
1984            Ok(s.get().name.as_ref().to_string())
1985        }
1986        Value::Str(s) => Ok(s.get().clone()),
1987        Value::Keyword(k) => Ok(k.get().name.as_ref().to_string()),
1988        other => Err(EvalError::Runtime(format!(
1989            "expected a symbol or string for namespace name, got {}",
1990            other.type_name()
1991        ))),
1992    }
1993}
1994
1995// ── protocol* ─────────────────────────────────────────────────────────────────
1996
1997/// Read a `{:name "m" :min-arity n :variadic b}` map into a `ProtocolMethod`.
1998fn protocol_method_of(spec: &Value) -> EvalResult<ProtocolMethod> {
1999    let Value::Map(m) = spec.unwrap_meta() else {
2000        return Err(EvalError::Runtime(format!(
2001            "protocol* method spec must be a map, got {}",
2002            spec.type_name()
2003        )));
2004    };
2005    let field = |k: &str| m.get(&Value::keyword(Keyword::simple(k)));
2006    let name: Arc<str> = match field("name") {
2007        Some(Value::Str(s)) => Arc::from(s.get().as_str()),
2008        Some(Value::Symbol(s)) => Arc::from(s.get().name.as_ref()),
2009        Some(Value::Keyword(k)) => Arc::from(k.get().name.as_ref()),
2010        _ => {
2011            return Err(EvalError::Runtime(
2012                "protocol* method spec needs a :name".into(),
2013            ));
2014        }
2015    };
2016    let min_arity = match field("min-arity") {
2017        Some(Value::Long(n)) if n >= 0 => n as usize,
2018        _ => 1,
2019    };
2020    let variadic = !matches!(
2021        field("variadic"),
2022        None | Some(Value::Nil) | Some(Value::Bool(false))
2023    );
2024    Ok(ProtocolMethod {
2025        name,
2026        min_arity,
2027        variadic,
2028    })
2029}
2030
2031/// `(protocol* Name method-specs extend-via-metadata?)` — mint a protocol.
2032///
2033/// `method-specs` evaluates to a sequence of `{:name :min-arity :variadic}`
2034/// maps. The protocol is created in the CURRENT namespace, which is the whole
2035/// reason this stays a special form: `Protocol.ns` is what qualifies a method
2036/// name for extend-via-metadata dispatch, and a builtin fn cannot see the
2037/// environment. The protocol is returned, not interned — `defprotocol` is the
2038/// Clojure macro that `def`s it along with a dispatch fn per method.
2039fn eval_protocol_star(args: &[Form], env: &mut Env) -> EvalResult {
2040    if args.len() < 2 {
2041        return Err(EvalError::Runtime(
2042            "protocol* requires a name and method specs".into(),
2043        ));
2044    }
2045    // Name metadata belongs on the var `defprotocol` binds, not here.
2046    let (name, _) = require_sym_meta(args, 0, "protocol*", env)?;
2047    let specs = crate::interp::eval::eval(&args[1], env)?;
2048    let specs: Vec<Value> = match specs.unwrap_meta() {
2049        Value::Vector(v) => v.get().iter().cloned().collect(),
2050        Value::Nil => Vec::new(),
2051        v => {
2052            return Err(EvalError::Runtime(format!(
2053                "protocol* method specs must be a vector, got {}",
2054                v.type_name()
2055            )));
2056        }
2057    };
2058    let methods = specs
2059        .iter()
2060        .map(protocol_method_of)
2061        .collect::<EvalResult<Vec<_>>>()?;
2062    let extend_via_metadata = match args.get(2) {
2063        Some(f) => !matches!(
2064            crate::interp::eval::eval(f, env)?,
2065            Value::Nil | Value::Bool(false)
2066        ),
2067        None => false,
2068    };
2069    let proto = Protocol::new(
2070        Arc::from(name.as_str()),
2071        env.current_ns.clone(),
2072        methods,
2073        extend_via_metadata,
2074    );
2075    Ok(Value::Protocol(GcPtr::new(proto)))
2076}
2077
2078/// Build a `CljxFn` from the tail of a method-impl list: `(name [params] body...)`.
2079/// `parts[0]` is the method name symbol (ignored here — caller handles it).
2080/// `parts[1]` is the params vector.
2081/// `parts[2..]` is the body.
2082/// Bring a defrecord's FIELDS into scope in a method body, as
2083/// `(let* [f (:f this) ...] body...)`.
2084///
2085/// The bare field symbol is the idiomatic form — `(mutable? [_] (valid-sha? sha))`
2086/// — and it read as an unbound symbol, because a method impl is built as an
2087/// ordinary fn whose only bindings are its own params. Clojure compiles the
2088/// fields as instance fields of the generated class, so they are simply in
2089/// scope.
2090///
2091/// A field whose name a PARAM already takes is skipped: the param shadows the
2092/// field in Clojure, and binding it here would shadow the param instead.
2093/// Returns None when there is nothing to bind, or when the first param is not
2094/// a plain symbol (a destructured `this` has no name to read the fields from).
2095fn synth_field_scope(
2096    params_form: &Form,
2097    fields: &[Arc<str>],
2098    mutable_fields: &[Arc<str>],
2099    body: &[Form],
2100) -> Option<Vec<Form>> {
2101    let param_forms = match &params_form.kind {
2102        FormKind::Vector(v) => v,
2103        _ => return None,
2104    };
2105    let this_name = match param_forms.first().map(|f| &f.kind) {
2106        Some(FormKind::Symbol(s)) => s.clone(),
2107        _ => return None,
2108    };
2109    let param_names: Vec<&str> = param_forms
2110        .iter()
2111        .filter_map(|f| match &f.kind {
2112            FormKind::Symbol(s) => Some(s.as_str()),
2113            _ => None,
2114        })
2115        .collect();
2116
2117    let span = params_form.span.clone();
2118    let is_mut = |name: &str| mutable_fields.iter().any(|m| m.as_ref() == name);
2119    let mut bindings: Vec<Form> = Vec::new();
2120    for field in fields {
2121        if param_names.contains(&field.as_ref()) {
2122            continue;
2123        }
2124        bindings.push(Form::new(FormKind::Symbol(field.to_string()), span.clone()));
2125        // A mutable field reads through `(.-field this)` — the live cell; an
2126        // immutable one through `(:field this)` — the field map. Both snapshot
2127        // at method entry; `set!` refreshes the local for later reads.
2128        let accessor = if is_mut(field) {
2129            FormKind::List(vec![
2130                Form::new(FormKind::Symbol(format!(".-{field}")), span.clone()),
2131                Form::new(FormKind::Symbol(this_name.clone()), span.clone()),
2132            ])
2133        } else {
2134            FormKind::List(vec![
2135                Form::new(FormKind::Keyword(field.to_string()), span.clone()),
2136                Form::new(FormKind::Symbol(this_name.clone()), span.clone()),
2137            ])
2138        };
2139        bindings.push(Form::new(accessor, span.clone()));
2140    }
2141    // With mutable fields present, bind a hidden handle to `this` so `set!` can
2142    // find the instance whose cell to update.
2143    if !mutable_fields.is_empty() {
2144        bindings.push(Form::new(
2145            FormKind::Symbol(DEFTYPE_SELF.to_string()),
2146            span.clone(),
2147        ));
2148        bindings.push(Form::new(FormKind::Symbol(this_name.clone()), span.clone()));
2149    }
2150    if bindings.is_empty() {
2151        return None;
2152    }
2153
2154    let mut let_forms = vec![
2155        Form::new(FormKind::Symbol("let*".to_string()), span.clone()),
2156        Form::new(FormKind::Vector(bindings), span.clone()),
2157    ];
2158    let_forms.extend_from_slice(body);
2159    Some(vec![Form::new(FormKind::List(let_forms), span)])
2160}
2161
2162fn build_impl_fn(
2163    parts: &[Form],
2164    fields: &[Arc<str>],
2165    mutable_fields: &[Arc<str>],
2166    env: &mut Env,
2167) -> EvalResult<Value> {
2168    if parts.len() < 2 {
2169        return Err(EvalError::Runtime(
2170            "protocol method impl requires params and body".into(),
2171        ));
2172    }
2173    // parts[1] should be the params vector.
2174    let params_form = &parts[1];
2175    let body = &parts[2..];
2176    let scoped;
2177    let body: &[Form] = if fields.is_empty() {
2178        body
2179    } else {
2180        match synth_field_scope(params_form, fields, mutable_fields, body) {
2181            Some(v) => {
2182                scoped = v;
2183                &scoped
2184            }
2185            None => body,
2186        }
2187    };
2188    let arity = parse_arity(params_form, body)?;
2189    let (closed_over_names, closed_over_vals) = env.all_local_bindings();
2190    let fn_name: Option<Arc<str>> = parts[0].as_symbol().map(Arc::from);
2191    let cljrs_fn = CljxFn::new(
2192        fn_name,
2193        vec![arity],
2194        closed_over_names,
2195        closed_over_vals,
2196        false,
2197        Arc::clone(&env.current_ns),
2198    );
2199    Ok(Value::Fn(GcPtr::new(cljrs_fn)))
2200}
2201
2202// ── binding ───────────────────────────────────────────────────────────────────
2203
2204fn eval_binding(args: &[Form], env: &mut Env) -> EvalResult {
2205    let pairs = match args.first().and_then(|f| f.as_vector()) {
2206        Some(v) => expand_pairs(v)
2207            .map_err(|_| EvalError::Runtime("binding vector must have even count".into()))?
2208            .into_owned(),
2209        None => return Err(EvalError::Runtime("binding requires a vector".into())),
2210    };
2211
2212    let mut frame: HashMap<usize, Value> = HashMap::new();
2213    for pair in pairs.chunks(2) {
2214        let Some(sym_str) = pair[0].as_symbol() else {
2215            return Err(EvalError::Runtime("binding targets must be symbols".into()));
2216        };
2217        let parsed = cljrs_value::Symbol::parse(sym_str);
2218        let ns_part: Arc<str> = env.resolve_ns_or_current(parsed.namespace.as_deref());
2219        let var_ptr = env
2220            .globals
2221            .lookup_var_in_ns(&ns_part, &parsed.name)
2222            .ok_or_else(|| EvalError::UnboundSymbol(sym_str.to_string()))?;
2223        let val = eval(&pair[1], env)?;
2224        frame.insert(crate::env::dynamics::var_key_of(&var_ptr), val);
2225    }
2226
2227    let _guard = crate::env::dynamics::push_frame(frame);
2228    eval_body(&args[1..], env)
2229    // _guard drops here → pop_frame()
2230}
2231
2232// ── deftype / defrecord shared construction ─────────────────────────────────────
2233
2234/// Hidden `let*` binding a `deftype` method body carries when the type has
2235/// mutable fields: a handle to `this`, so `set!` can locate the instance whose
2236/// interior-mutable cell to update.
2237const DEFTYPE_SELF: &str = "__deftype_self__";
2238
2239/// Does a field's `^meta` mark it `^:unsynchronized-mutable` or
2240/// `^:volatile-mutable`? Instances are single-threaded here, so the two are
2241/// treated identically — only whether the field is mutable at all matters.
2242fn meta_form_is_mutable(meta: &Form) -> bool {
2243    let is_mut_kw = |k: &str| k == "unsynchronized-mutable" || k == "volatile-mutable";
2244    match &meta.kind {
2245        FormKind::Keyword(k) => is_mut_kw(k),
2246        FormKind::Map(entries) => entries.chunks(2).any(|kv| {
2247            matches!(&kv[0].kind, FormKind::Keyword(k) if is_mut_kw(k))
2248                && !matches!(
2249                    kv.get(1).map(|f| &f.kind),
2250                    None | Some(FormKind::Bool(false)) | Some(FormKind::Nil)
2251                )
2252        }),
2253        // Metadata that reached this form through a macro is QUOTED: it is
2254        // already a value, and re-analysing it would resolve its contents as
2255        // code (see `value_to_form`). A field vector written in source arrives
2256        // unquoted; one a macro emitted arrives quoted. Both describe the same
2257        // field, so both have to be read.
2258        FormKind::Quote(inner) => meta_form_is_mutable(inner),
2259        _ => false,
2260    }
2261}
2262
2263/// A single field spec: its name and whether it is mutable.
2264fn field_spec_of(form: &Form) -> Option<(Arc<str>, bool)> {
2265    match &form.kind {
2266        FormKind::Symbol(s) => Some((Arc::from(s.as_str()), false)),
2267        FormKind::Meta(meta, inner) => {
2268            let here = meta_form_is_mutable(meta);
2269            field_spec_of(inner).map(|(name, inner_mut)| (name, inner_mut || here))
2270        }
2271        _ => None,
2272    }
2273}
2274
2275/// Parse a `deftype` `[field ...]` vector into `(name, mutable?)` specs.
2276fn parse_field_specs(form: &Form, ctx: &str) -> EvalResult<Vec<(Arc<str>, bool)>> {
2277    // `as_vector` reports the shape under any `^meta`, so a marker on the
2278    // vector itself — `(defrecord R ^:marker [x])` — stays transparent.
2279    let Some(fields) = form.as_vector() else {
2280        return Err(EvalError::Runtime(format!(
2281            "{ctx} requires a field vector as second arg"
2282        )));
2283    };
2284    fields
2285        .iter()
2286        .map(|f| {
2287            field_spec_of(f)
2288                .ok_or_else(|| EvalError::Runtime(format!("{ctx} field names must be symbols")))
2289        })
2290        .collect()
2291}
2292
2293// ── deftype* (defrecord / deftype are bootstrap macros over it) ─────────────────
2294
2295/// The irreducible datatype primitive: mint a type tag and register method impls
2296/// against it, with the fields in scope in each body and mutable fields backed by
2297/// live cells. It does NOT synthesise constructors or intern the type symbol —
2298/// that sugar lives in the `deftype` bootstrap macro (`->T`, `(def T 'T)`), which
2299/// is what makes deftype a particular case of a Clojure macro over this form.
2300fn eval_deftype_star(args: &[Form], env: &mut Env) -> EvalResult {
2301    // (deftype* TypeName [field ...] Proto (method [this] body) ...)
2302    if args.len() < 2 {
2303        return Err(EvalError::Runtime(
2304            "deftype* requires a name and field vector".into(),
2305        ));
2306    }
2307    // Type metadata (e.g. ^:private) has no var to hold it; unwrapped so the
2308    // name reads, and deliberately not applied anywhere it would not belong.
2309    let (type_name, _) = require_sym_meta(args, 0, "deftype*", env)?;
2310    let type_tag = qualified_type_tag(&type_name, env);
2311
2312    let specs = parse_field_specs(&args[1], "deftype*")?;
2313    let field_names: Vec<Arc<str>> = specs.iter().map(|(n, _)| n.clone()).collect();
2314    let mutable_names: Vec<Arc<str>> = specs
2315        .iter()
2316        .filter(|(_, m)| *m)
2317        .map(|(n, _)| n.clone())
2318        .collect();
2319
2320    // Register protocol/interface method impls, with the fields in scope in
2321    // each body — same machinery as defrecord/reify. Mutable fields read
2322    // through the live cell and are writable with `set!`.
2323    register_impls_for_tag(&type_tag, &args[2..], &field_names, &mutable_names, env)?;
2324    // Return the minted tag so a caller (e.g. the `reify` macro) can feed it
2325    // straight to `make-type-instance` — a single dataflow source for the tag,
2326    // rather than a second textual reference that a gensym could desync.
2327    Ok(Value::string(type_tag.to_string()))
2328}
2329
2330/// The dispatch tag of a record or deftype: its name qualified by the defining
2331/// namespace, `my.ns.Point`, as the JVM names the generated class. Two
2332/// namespaces may each define a `Point`; with a bare-name tag the second one's
2333/// protocol impls replaced the first one's, and `instance?` could not tell
2334/// their instances apart.
2335fn qualified_type_tag(type_name: &str, env: &Env) -> Arc<str> {
2336    Arc::from(format!("{}.{}", env.current_ns, type_name))
2337}
2338
2339// ── register_impls_for_tag ────────────────────────────────────────────────────
2340
2341/// Resolve a protocol NAME symbol in an impl position (extend-type, extend-protocol,
2342/// reify/defrecord), honouring the current namespace's `:require :as` aliases and
2343/// fully-qualified names — not just an unqualified lookup in the current ns.
2344///
2345/// `(defrecord R [] mp/IThing (-do [_] ...))` failed with "mp/IThing is not a
2346/// protocol" even though the protocol was loaded and (resolve 'mini.proto/IThing)
2347/// was truthy: the old code looked up the whole string "mp/IThing" as an intern of
2348/// the CURRENT ns, where it is neither interned nor referred. A qualified protocol
2349/// symbol must resolve through its own namespace, exactly as `eval` resolves any
2350/// other qualified symbol.
2351fn resolve_protocol_sym(env: &Env, s: &str) -> Option<GcPtr<Protocol>> {
2352    let parsed = cljrs_value::Symbol::parse(s);
2353    let val = match parsed.namespace.as_deref() {
2354        Some(ns_part) => {
2355            let ns = env.resolve_ns_part(ns_part);
2356            env.globals.lookup_in_ns(&ns, &parsed.name)
2357        }
2358        None => env.globals.lookup_in_ns(&env.current_ns, s),
2359    };
2360    match val {
2361        Some(Value::Protocol(p)) => Some(p),
2362        _ => None,
2363    }
2364}
2365
2366/// Parse `Proto (method [params] body) ...` segments and register them under `type_tag`.
2367/// Shared by `defrecord` and `reify`.
2368fn register_impls_for_tag(
2369    type_tag: &Arc<str>,
2370    forms: &[Form],
2371    fields: &[Arc<str>],
2372    mutable_fields: &[Arc<str>],
2373    env: &mut Env,
2374) -> EvalResult<()> {
2375    let mut current_proto: Option<GcPtr<cljrs_value::Protocol>> = None;
2376
2377    for form in forms {
2378        match &form.unmeta().kind {
2379            FormKind::Symbol(s) => match resolve_protocol_sym(env, s) {
2380                Some(p) => current_proto = Some(p),
2381                None => {
2382                    return Err(EvalError::Runtime(format!(
2383                        "reify/defrecord: {} is not a protocol",
2384                        s
2385                    )));
2386                }
2387            },
2388            FormKind::List(parts) => {
2389                let proto = current_proto.as_ref().ok_or_else(|| {
2390                    EvalError::Runtime("reify/defrecord: method impl before protocol name".into())
2391                })?;
2392                if parts.is_empty() {
2393                    continue;
2394                }
2395                let method_name: Arc<str> = match parts[0].as_symbol() {
2396                    Some(s) => Arc::from(s),
2397                    None => continue,
2398                };
2399                let fn_val = build_impl_fn(parts, fields, mutable_fields, env)?;
2400                let mut impls = proto.get().impls.lock().unwrap();
2401                impls
2402                    .entry(type_tag.clone())
2403                    .or_default()
2404                    .insert(method_name, fn_val);
2405                drop(impls);
2406                cljrs_value::bump_protocol_generation();
2407            }
2408            _ => {}
2409        }
2410    }
2411    Ok(())
2412}
2413
2414// ── helpers ───────────────────────────────────────────────────────────────────
2415
2416/// Update the root binding of `*ns*` in `clojure.core` to the current namespace.
2417/// Called whenever `env.current_ns` changes (ns, in-ns, standard_env setup).
2418pub fn sync_star_ns(env: &mut Env) {
2419    if let Some(star_ns_var) = env.globals.lookup_var("clojure.core", "*ns*") {
2420        let ns_ptr = env.globals.get_or_create_ns(&env.current_ns);
2421        star_ns_var.get().bind(Value::Namespace(ns_ptr));
2422    }
2423}
2424
2425/// The symbol at `idx`, unwrapping any `^meta` wrapper, together with the
2426/// metadata it carried.
2427///
2428/// `(defprotocol ^:private Driver ...)` reads as `Meta(:private, Symbol("Driver"))`,
2429/// so matching `FormKind::Symbol` alone rejects a form Clojure accepts. malli's
2430/// `malli.impl.regex` opens with five such protocols, which made all of malli
2431/// unloadable.
2432///
2433/// The metadata is RETURNED rather than discarded: dropping it would trade a
2434/// loud error for a silent loss of `^:private`, and callers that intern a var
2435/// attach it there.
2436fn require_sym_meta(
2437    args: &[Form],
2438    idx: usize,
2439    form_name: &str,
2440    env: &mut Env,
2441) -> EvalResult<(String, Option<Value>)> {
2442    fn peel(form: &Form, env: &mut Env) -> EvalResult<Option<(String, Option<Value>)>> {
2443        match &form.kind {
2444            FormKind::Symbol(s) => Ok(Some((s.clone(), None))),
2445            // `^:a ^:b x` nests Meta forms; unwrap all, outer mark winning.
2446            FormKind::Meta(meta_form, inner) => {
2447                let meta_val = compile_meta_form(meta_form, env)?;
2448                match peel(inner, env)? {
2449                    Some((name, inner_meta)) => {
2450                        Ok(Some((name, merge_meta(inner_meta, Some(meta_val)))))
2451                    }
2452                    None => Ok(None),
2453                }
2454            }
2455            _ => Ok(None),
2456        }
2457    }
2458    let err = || EvalError::Runtime(format!("{form_name} requires a symbol at position {idx}"));
2459    match args.get(idx) {
2460        Some(form) => peel(form, env)?.ok_or_else(err),
2461        None => Err(err()),
2462    }
2463}
2464
2465// ── with-out-str ──────────────────────────────────────────────────────────────
2466
2467fn eval_with_out_str(body: &[Form], env: &mut Env) -> EvalResult {
2468    crate::builtins::builtins::push_output_capture();
2469    let result = eval_body(body, env);
2470    let captured = crate::builtins::builtins::pop_output_capture().unwrap_or_default();
2471    // Propagate errors but still pop the capture buffer
2472    result?;
2473    Ok(Value::string(captured))
2474}
2475
2476// ── await ─────────────────────────────────────────────────────────────────────
2477
2478/// Blocking deref in sync context; yielding deref in async context.
2479///
2480/// When `cljrs-async` is loaded, `eval_async` intercepts `await` forms before
2481/// the sync evaluator reaches this handler, so this path is only taken in
2482/// non-async code. Inside a fiber, this path parks until settlement.
2483/// Outside a fiber, it blocks the OS thread, like `(deref val)`.
2484fn eval_await(args: &[Form], env: &mut Env) -> EvalResult {
2485    if args.is_empty() {
2486        return Err(EvalError::Runtime("await requires one argument".into()));
2487    }
2488    let val = eval(&args[0], env)?;
2489    match val {
2490        Value::Future(_) | Value::Promise(_) => crate::interp::eval::deref_value(val),
2491        other => Ok(other),
2492    }
2493}