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