Skip to main content

cljrs_runtime/interp/
eval.rs

1//! Top-level `eval` dispatcher and form-to-value conversion.
2
3use std::sync::Arc;
4
5use crate::builtins::form::{expand_pairs, expand_reader_conds, expand_reader_conds_cow};
6use crate::builtins::special::SPECIAL_FORMS;
7use crate::env::env::{Env, GlobalEnv};
8use crate::env::error::{EvalError, EvalResult};
9use crate::interp::apply::eval_call;
10use crate::interp::special::eval_special;
11use crate::interp::syntax_quote::syntax_quote;
12use cljrs_gc::GcPtr;
13use cljrs_reader::Form;
14use cljrs_reader::form::FormKind;
15use cljrs_value::regex::Pattern;
16use cljrs_value::value::SetValue;
17use cljrs_value::{
18    CljxFuture, FutureState, Keyword, MapValue, PersistentHashSet, PersistentList,
19    PersistentVector, Symbol, Value,
20};
21
22/// Evaluate a `Form` in the given `Env`.
23pub fn eval(form: &Form, env: &mut Env) -> EvalResult {
24    if !crate::env::gas::charge(1) {
25        return Err(EvalError::GasExhausted);
26    }
27    match &form.kind {
28        // ── Atoms ─────────────────────────────────────────────────────────
29        FormKind::Nil => Ok(Value::Nil),
30        FormKind::Bool(b) => Ok(Value::Bool(*b)),
31        FormKind::Int(n) => Ok(Value::Long(*n)),
32        FormKind::Float(f) => Ok(Value::Double(*f)),
33        FormKind::Symbolic(f) => Ok(Value::Double(*f)), // ##Inf etc.
34        FormKind::Str(s) => Ok(Value::string(s.clone())),
35        FormKind::Char(c) => Ok(Value::Char(*c)),
36        FormKind::BigInt(s) => crate::builtins::parse_bigint(s),
37        FormKind::BigDecimal(s) => crate::builtins::parse_bigdecimal(s),
38        FormKind::Ratio(s) => crate::builtins::parse_ratio(s),
39        FormKind::Regex(s) => {
40            let r = Pattern::new(s);
41            match r {
42                Ok(r) => Ok(Value::Pattern(GcPtr::new(r))),
43                Err(e) => Err(EvalError::Runtime(e.to_string())),
44            }
45        }
46
47        // ── Identifiers ───────────────────────────────────────────────────
48        FormKind::Symbol(s) => eval_symbol(s, env),
49        FormKind::Keyword(s) => Ok(Value::keyword(Keyword::parse(s))),
50        FormKind::AutoKeyword(s) => {
51            let full = env
52                .globals
53                .resolve_auto_keyword(&env.current_ns, s)
54                .map_err(EvalError::Runtime)?;
55            Ok(Value::keyword(Keyword::parse(&full)))
56        }
57        // A symbol key from an auto-resolved namespaced map: in evaluated
58        // position it names a var, exactly as a written-out symbol key does.
59        FormKind::AutoSymbol(s) => {
60            let full = env
61                .globals
62                .resolve_auto_keyword(&env.current_ns, s)
63                .map_err(EvalError::Runtime)?;
64            eval_symbol(&full, env)
65        }
66
67        // ── Collections ───────────────────────────────────────────────────
68        FormKind::List(forms) => eval_list(forms, env),
69        FormKind::Vector(forms) => {
70            let forms = expand_reader_conds_cow(forms);
71            let mut vals: Vec<Value> = Vec::with_capacity(forms.len());
72            for f in forms.iter() {
73                let _root = crate::env::gc_roots::root_values(&vals);
74                vals.push(eval(f, env)?);
75            }
76            Ok(Value::Vector(GcPtr::new(PersistentVector::from_iter(vals))))
77        }
78        FormKind::Map(forms) => {
79            let forms = expand_pairs(forms).map_err(|_| {
80                EvalError::Runtime("map literal must have an even number of forms".into())
81            })?;
82            let mut pairs: Vec<Value> = Vec::with_capacity(forms.len());
83            for f in forms.iter() {
84                let _root = crate::env::gc_roots::root_values(&pairs);
85                pairs.push(eval(f, env)?);
86            }
87            let kv_pairs: Vec<(Value, Value)> = pairs
88                .chunks(2)
89                .map(|pair| (pair[0].clone(), pair[1].clone()))
90                .collect();
91            Ok(Value::Map(MapValue::from_pairs(kv_pairs)))
92        }
93        FormKind::Set(forms) => {
94            let forms = expand_reader_conds_cow(forms);
95            let mut vals: Vec<Value> = Vec::with_capacity(forms.len());
96            for f in forms.iter() {
97                let _root = crate::env::gc_roots::root_values(&vals);
98                vals.push(eval(f, env)?);
99            }
100            Ok(Value::Set(SetValue::Hash(GcPtr::new(
101                PersistentHashSet::from_iter(vals),
102            ))))
103        }
104
105        // ── Reader macros ─────────────────────────────────────────────────
106        // `'x` sugar: like the `quote` special form, `::kw` and an
107        // auto-resolved map's symbol keys resolve against the reading
108        // namespace before the form becomes data.
109        FormKind::Quote(inner) => {
110            let resolved = crate::builtins::form::resolve_auto_forms(inner, env)?;
111            crate::builtins::form::form_to_value(&resolved)
112        }
113        FormKind::SyntaxQuote(inner) => syntax_quote(inner, env),
114        FormKind::Unquote(_) => Err(EvalError::Runtime("unquote outside syntax-quote".into())),
115        FormKind::UnquoteSplice(_) => Err(EvalError::Runtime(
116            "unquote-splice outside syntax-quote".into(),
117        )),
118        FormKind::Deref(inner) => {
119            let v = eval(inner, env)?;
120            if env.is_async && matches!(v, Value::Future(_)) {
121                return Err(EvalError::Runtime(
122                    "deref (@) on a future is not allowed inside an ^:async function; use (await ...) instead".into(),
123                ));
124            }
125            deref_value(v)
126        }
127        FormKind::Var(inner) => {
128            if let FormKind::Symbol(s) = &inner.kind {
129                let parsed = Symbol::parse(s);
130                let ns: Arc<str> = match parsed.namespace.as_deref() {
131                    Some(ns_part) => env
132                        .globals
133                        .resolve_alias(&env.current_ns, ns_part)
134                        .unwrap_or_else(|| Arc::from(ns_part)),
135                    None => env.current_ns.clone(),
136                };
137                env.globals
138                    .lookup_var_in_ns(&ns, &parsed.name)
139                    .map(Value::Var)
140                    .ok_or_else(|| EvalError::UnboundSymbol(s.clone()))
141            } else {
142                Err(EvalError::Runtime("var requires a symbol".into()))
143            }
144        }
145        FormKind::Meta(meta, form) => {
146            // `^m expr` evaluates `expr`; the annotation becomes runtime
147            // metadata only on a form that constructs an `IObj` (a collection
148            // literal or an `fn`). Everywhere else it is a compile-time hint,
149            // and — as on the JVM — is not evaluated at all.
150            //
151            // `lower::anf` applies the same rule, so a promoted or AOT-compiled
152            // body answers `meta` the same way an interpreted one does.
153            let value = eval(form, env)?;
154            if !form.takes_runtime_meta() {
155                return Ok(value);
156            }
157            let m = crate::builtins::form::expand_meta_annotation(meta, &mut |f| eval(f, env))?;
158            Ok(crate::builtins::form::attach_meta(value, m))
159        }
160
161        // ── Dispatch ──────────────────────────────────────────────────────
162        FormKind::AnonFn(body) => {
163            let expanded = crate::builtins::form::expand_anon_fn(body, form.span.clone());
164            eval(&expanded, env)
165        }
166        FormKind::ReaderCond {
167            splicing: _,
168            clauses,
169        } => eval_reader_cond(clauses, env),
170        FormKind::TaggedLiteral(tag, inner) => eval_tagged_literal(tag, inner, env),
171    }
172}
173
174/// Evaluate a form with a cooperative execution-credit budget.
175///
176/// Nested tree-walker, IR-interpreter, and JIT work shares this budget.  The
177/// existing [`eval`] entry point remains unmetered unless called inside this
178/// dynamic scope.
179pub fn eval_with_gas(form: &Form, env: &mut Env, credits: u64) -> EvalResult {
180    let meter = crate::env::gas::GasMeter::new(credits);
181    let _guard = crate::env::gas::GasGuard::install(meter);
182    eval(form, env)
183}
184
185// ── List / call dispatch ──────────────────────────────────────────────────────
186
187fn eval_list(forms: &[Form], env: &mut Env) -> EvalResult {
188    if forms.is_empty() {
189        return Ok(Value::List(GcPtr::new(PersistentList::empty())));
190    }
191
192    // Expand reader conditionals (both splicing and non-splicing) before dispatch.
193    let expanded: Vec<Form>;
194    let forms: &[Form] = if forms
195        .iter()
196        .any(|f| matches!(f.kind, FormKind::ReaderCond { .. }))
197    {
198        expanded = expand_reader_conds(forms);
199        if expanded.is_empty() {
200            return Ok(Value::List(GcPtr::new(PersistentList::empty())));
201        }
202        &expanded
203    } else {
204        forms
205    };
206
207    // Check for special form.
208    if let Some(s) = forms[0].as_symbol()
209        && is_special_form(s)
210    {
211        return eval_special(s, &forms[1..], env);
212    }
213
214    eval_call(&forms[0], &forms[1..], env)
215}
216
217// ── Symbol resolution ─────────────────────────────────────────────────────────
218
219fn eval_symbol(s: &str, env: &mut Env) -> EvalResult {
220    let sym = Symbol::parse(s);
221
222    // Explicit version suffix (`name@hash` or `ns/name@hash`): always a
223    // namespace-level lookup — no local-frame fallback.
224    #[cfg(not(target_arch = "wasm32"))]
225    if let Some(ref commit) = sym.version.clone() {
226        crate::env::policy::check_versioned_lookup()?;
227        return crate::interp::versioned::resolve_versioned_symbol(&sym, commit, env);
228    }
229    #[cfg(target_arch = "wasm32")]
230    if sym.version.is_some() {
231        return Err(crate::env::error::EvalError::Runtime(
232            "versioned symbols are not supported in WASM".to_string(),
233        ));
234    }
235
236    // Local frames (params, let-bindings, closed-over vars) take priority for
237    // unversioned symbols.
238    if let Some(v) = env.lookup_local_frames(s) {
239        return Ok(v);
240    }
241
242    // Inherited versioned context: if we are evaluating inside a versioned
243    // function body, unversioned same-namespace symbols resolve at the inherited
244    // commit rather than HEAD.  "Same namespace" includes a qualified
245    // self-reference written with the base name (`mylib/x` inside `mylib@hash`).
246    #[cfg(not(target_arch = "wasm32"))]
247    if let Some(commit) = env.versioned_eval_commit.clone() {
248        let is_same_ns = sym.namespace.is_none()
249            || sym.namespace.as_deref() == Some(env.current_ns.as_ref())
250            || sym.namespace.as_deref()
251                == Some(crate::env::versioned::base_ns_name(&env.current_ns));
252        if is_same_ns {
253            return crate::interp::versioned::resolve_versioned_symbol(&sym, &commit, env);
254        }
255    }
256
257    // Fall through to normal global namespace lookup.
258    if let Some(v) = env.globals.lookup_in_ns(&env.current_ns, s) {
259        return Ok(v);
260    }
261
262    // Namespace-qualified external symbol: `ns/name`
263    if s.contains('/')
264        && !s.starts_with('/')
265        && let Some(ns_part) = &sym.namespace
266    {
267        let resolved: Arc<str> = env
268            .globals
269            .resolve_alias(&env.current_ns, ns_part)
270            .unwrap_or_else(|| Arc::from(ns_part.as_ref()));
271        // Qualified self-reference inside a versioned namespace: `mylib/x`
272        // written in `mylib@hash`'s own source resolves at the pinned commit,
273        // i.e. inside the versioned namespace itself.
274        #[cfg(not(target_arch = "wasm32"))]
275        let resolved: Arc<str> = if env.current_ns.as_ref() != resolved.as_ref()
276            && crate::env::versioned::base_ns_name(&env.current_ns) == resolved.as_ref()
277        {
278            env.current_ns.clone()
279        } else {
280            resolved
281        };
282        if resolved.as_ref() != env.current_ns.as_ref()
283            && let Some(var) = env.globals.lookup_var(&resolved, &sym.name)
284            && GlobalEnv::var_is_private(var.get())
285        {
286            return Err(EvalError::Runtime(format!(
287                "var: {}/{} is not public",
288                resolved, sym.name
289            )));
290        }
291        return env
292            .globals
293            .lookup_in_ns(&resolved, &sym.name)
294            .ok_or_else(|| EvalError::UnboundSymbol(s.to_string()));
295    }
296
297    // JVM class names resolve to themselves as symbols (for instance?, catch, etc.)
298    if is_jvm_class_name(s) {
299        return Ok(Value::symbol(Symbol::simple(s)));
300    }
301
302    Err(EvalError::UnboundSymbol(s.to_string()))
303}
304
305/// Recognise JVM-style class names used in Clojure for `instance?`, `catch`, etc.
306pub fn is_jvm_class_name(s: &str) -> bool {
307    matches!(
308        s,
309        "clojure.lang.BigInt"
310            | "java.math.BigDecimal"
311            | "java.math.BigInteger"
312            | "clojure.lang.Ratio"
313            | "java.lang.Long"
314            | "java.lang.Double"
315            | "java.lang.String"
316            | "java.lang.Boolean"
317            | "java.lang.Character"
318            | "java.lang.Number"
319            | "clojure.lang.Symbol"
320            | "clojure.lang.Keyword"
321            | "clojure.lang.PersistentList"
322            | "clojure.lang.PersistentVector"
323            | "clojure.lang.PersistentHashMap"
324            | "clojure.lang.PersistentHashSet"
325            | "clojure.lang.PersistentArrayMap"
326            | "clojure.lang.IFn"
327            | "clojure.lang.ISeq"
328            | "clojure.lang.IPending"
329            | "clojure.lang.Atom"
330            | "clojure.lang.Var"
331            | "clojure.lang.Namespace"
332            | "java.util.UUID"
333            | "java.lang.Exception"
334            | "java.lang.Throwable"
335            | "java.lang.Error"
336            | "Exception"
337            | "Throwable"
338            | "Error"
339            | "clojure.lang.ExceptionInfo"
340            | "clojure.lang.IEditableCollection"
341            | "Boolean"
342            | "clojure.lang.PersistentQueue"
343            | "java.util.regex.Pattern"
344    )
345}
346
347// ── is_special_form ───────────────────────────────────────────────────────────
348
349pub fn is_special_form(s: &str) -> bool {
350    SPECIAL_FORMS.contains(&s)
351}
352
353// ── eval_body ─────────────────────────────────────────────────────────────────
354
355/// Dereference a value: used by `@x` reader macro and the `deref` builtin.
356pub fn deref_value(v: Value) -> EvalResult {
357    match v {
358        Value::Atom(a) => Ok(a.get().deref()),
359        Value::SharedAtom(sa) => Ok(cljrs_value::demote(&sa.deref_val())),
360        Value::Var(var) => crate::env::dynamics::deref_var(&var)
361            .ok_or_else(|| EvalError::Runtime("unbound var".into())),
362        Value::Volatile(vol) => Ok(vol.get().deref()),
363        Value::Delay(d) => d.get().force().map_err(EvalError::Runtime),
364        Value::Agent(a) => Ok(a.get().get_state()),
365        Value::Reduced(inner) => Ok(*inner),
366        Value::Promise(p) => Ok(p.get().deref_blocking()),
367        Value::Future(f) => {
368            let mut guard = f.get().state.lock().unwrap();
369            loop {
370                match &*guard {
371                    FutureState::Done(v) => {
372                        f.get().mark_observed();
373                        return Ok(v.clone());
374                    }
375                    FutureState::Failed(v) => {
376                        f.get().mark_observed();
377                        return Err(EvalError::Thrown(v.clone()));
378                    }
379                    FutureState::GasExhausted => {
380                        f.get().mark_observed();
381                        return Err(EvalError::GasExhausted);
382                    }
383                    FutureState::Cancelled => {
384                        return Err(EvalError::Thrown(CljxFuture::cancelled_error()));
385                    }
386                    FutureState::Running => {
387                        guard = f.get().cond.wait(guard).unwrap();
388                    }
389                }
390            }
391        }
392        other => Err(EvalError::Runtime(format!(
393            "cannot deref {}",
394            other.type_name()
395        ))),
396    }
397}
398
399/// Evaluate a sequence of forms and return the value of the last one.
400pub fn eval_body(forms: &[Form], env: &mut Env) -> EvalResult {
401    let mut result = Value::Nil;
402    for form in forms {
403        result = eval(form, env)?;
404    }
405    Ok(result)
406}
407
408// ── reader cond ───────────────────────────────────────────────────────────────
409
410fn eval_reader_cond(clauses: &[Form], env: &mut Env) -> EvalResult {
411    // clauses = [kw form kw form ...]
412    let mut i = 0;
413    let mut default: Option<&Form> = None;
414    while i + 1 < clauses.len() {
415        match &clauses[i].kind {
416            FormKind::Keyword(k) if k == "rust" => {
417                return eval(&clauses[i + 1], env);
418            }
419            FormKind::Keyword(k) if k == "default" => {
420                default = Some(&clauses[i + 1]);
421            }
422            _ => {}
423        }
424        i += 2;
425    }
426    match default {
427        Some(f) => eval(f, env),
428        None => Ok(Value::Nil),
429    }
430}
431
432// ── tagged literals ──────────────────────────────────────────────────────────
433
434fn eval_tagged_literal(tag: &str, inner: &Form, env: &mut Env) -> EvalResult {
435    match tag {
436        "uuid" => {
437            let val = eval(inner, env)?;
438            match &val {
439                Value::Str(s) => {
440                    let uuid = uuid::Uuid::parse_str(s.get())
441                        .map_err(|e| EvalError::Runtime(format!("invalid UUID: {e}")))?;
442                    Ok(Value::Uuid(uuid.as_u128()))
443                }
444                _ => Err(EvalError::Runtime(format!(
445                    "#uuid expects a string, got {}",
446                    val.type_name()
447                ))),
448            }
449        }
450        "inst" => {
451            // TODO: implement #inst for date/time literals
452            let val = eval(inner, env)?;
453            Ok(val)
454        }
455        _ => Err(EvalError::Runtime(format!(
456            "unknown tagged literal: #{tag}"
457        ))),
458    }
459}
460
461// ── Tests ─────────────────────────────────────────────────────────────────────
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use crate::env::env::GlobalEnv;
467    use std::sync::Arc;
468
469    fn make_env() -> (Arc<GlobalEnv>, Env) {
470        let globals = crate::Runtime::builder()
471            .execution_mode(crate::ExecutionMode::TreeWalk)
472            .eager_clojure_test(true)
473            .build()
474            .expect("runtime")
475            .into_globals();
476        let env = Env::new(globals.clone(), "user");
477        (globals, env)
478    }
479
480    fn eval_str(src: &str) -> EvalResult {
481        let (_, mut env) = make_env();
482        eval_src(src, &mut env)
483    }
484
485    fn eval_src(src: &str, env: &mut Env) -> EvalResult {
486        let mut parser = cljrs_reader::Parser::new(src.to_string(), "<test>".to_string());
487        let forms = parser.parse_all().map_err(EvalError::Read)?;
488        let mut result = Value::Nil;
489        for form in forms {
490            result = eval(&form, env)?;
491        }
492        Ok(result)
493    }
494
495    fn long(n: i64) -> Value {
496        Value::Long(n)
497    }
498    fn bool_v(b: bool) -> Value {
499        Value::Bool(b)
500    }
501
502    // ── Atoms ─────────────────────────────────────────────────────────────
503
504    #[test]
505    fn test_literal_int() {
506        assert_eq!(eval_str("42").unwrap(), long(42));
507    }
508
509    #[test]
510    fn test_literal_string() {
511        assert!(matches!(eval_str("\"hello\"").unwrap(), Value::Str(_)));
512    }
513
514    #[test]
515    fn test_literal_nil() {
516        assert_eq!(eval_str("nil").unwrap(), Value::Nil);
517    }
518
519    #[test]
520    fn test_literal_true() {
521        assert_eq!(eval_str("true").unwrap(), bool_v(true));
522    }
523
524    #[test]
525    fn test_literal_false() {
526        assert_eq!(eval_str("false").unwrap(), bool_v(false));
527    }
528
529    // ── Arithmetic ────────────────────────────────────────────────────────
530
531    #[test]
532    fn test_add() {
533        assert_eq!(eval_str("(+ 1 2)").unwrap(), long(3));
534    }
535
536    #[test]
537    fn test_mul() {
538        assert_eq!(eval_str("(* 2 3)").unwrap(), long(6));
539    }
540
541    #[test]
542    fn test_div_exact() {
543        assert_eq!(eval_str("(/ 10 2)").unwrap(), long(5));
544    }
545
546    #[test]
547    fn test_sub() {
548        assert_eq!(eval_str("(- 10 3)").unwrap(), long(7));
549    }
550
551    // ── let ───────────────────────────────────────────────────────────────
552
553    #[test]
554    fn test_let_simple() {
555        assert_eq!(eval_str("(let* [x 1 y 2] (+ x y))").unwrap(), long(3));
556    }
557
558    #[test]
559    fn test_let_shadowing() {
560        assert_eq!(eval_str("(let* [x 1] (let* [x 10] x))").unwrap(), long(10));
561    }
562
563    // ── fn + call ─────────────────────────────────────────────────────────
564
565    #[test]
566    fn test_fn_call() {
567        assert_eq!(eval_str("((fn* [x] (* x x)) 5)").unwrap(), long(25));
568    }
569
570    #[test]
571    fn test_closure_capture() {
572        assert_eq!(
573            eval_str("(let* [n 3] ((fn* [x] (+ x n)) 4))").unwrap(),
574            long(7)
575        );
576    }
577
578    #[test]
579    fn test_multi_arity_fn() {
580        assert_eq!(
581            eval_str("((fn* ([x] x) ([x y] (+ x y))) 1 2)").unwrap(),
582            long(3)
583        );
584    }
585
586    // ── recur / loop ──────────────────────────────────────────────────────
587
588    #[test]
589    fn test_loop_recur() {
590        let result =
591            eval_str("(loop* [i 0 acc 0] (if (= i 5) acc (recur (inc i) (+ acc i))))").unwrap();
592        assert_eq!(result, long(10));
593    }
594
595    // ── def / defn ────────────────────────────────────────────────────────
596
597    #[test]
598    fn test_def() {
599        let (_, mut env) = make_env();
600        eval_src("(def x 42)", &mut env).unwrap();
601        assert_eq!(eval_src("x", &mut env).unwrap(), long(42));
602    }
603
604    #[test]
605    fn test_defn() {
606        let (_, mut env) = make_env();
607        eval_src("(defn square [x] (* x x))", &mut env).unwrap();
608        assert_eq!(eval_src("(square 7)", &mut env).unwrap(), long(49));
609    }
610
611    // ── if ────────────────────────────────────────────────────────────────
612
613    #[test]
614    fn test_if_truthy() {
615        assert_eq!(eval_str("(if true 1 2)").unwrap(), long(1));
616    }
617
618    #[test]
619    fn test_if_falsy() {
620        assert_eq!(eval_str("(if false 1 2)").unwrap(), long(2));
621    }
622
623    #[test]
624    fn test_if_nil_branch() {
625        assert_eq!(eval_str("(if false 1)").unwrap(), Value::Nil);
626    }
627
628    // ── do ────────────────────────────────────────────────────────────────
629
630    #[test]
631    fn test_do() {
632        assert_eq!(eval_str("(do 1 2 3)").unwrap(), long(3));
633    }
634
635    // ── quote ─────────────────────────────────────────────────────────────
636
637    #[test]
638    fn test_quote_list() {
639        let v = eval_str("'(1 2 3)").unwrap();
640        assert!(matches!(v, Value::List(_)));
641    }
642
643    // ── keyword lookup ────────────────────────────────────────────────────
644
645    #[test]
646    fn test_keyword_lookup() {
647        assert_eq!(eval_str("(:a {:a 1})").unwrap(), long(1));
648    }
649
650    #[test]
651    fn test_keyword_lookup_missing() {
652        assert_eq!(eval_str("(:b {:a 1})").unwrap(), Value::Nil);
653    }
654
655    // ── Map / Vector / Set literals ───────────────────────────────────────
656
657    #[test]
658    fn test_map_literal() {
659        let v = eval_str("{:a 1 :b 2}").unwrap();
660        assert!(matches!(v, Value::Map(_)));
661        if let Value::Map(m) = &v {
662            assert_eq!(m.count(), 2);
663        }
664    }
665
666    #[test]
667    fn test_vector_literal() {
668        let v = eval_str("[1 2 3]").unwrap();
669        assert!(matches!(v, Value::Vector(_)));
670    }
671
672    #[test]
673    fn test_set_literal() {
674        let v = eval_str("#{1 2 3}").unwrap();
675        assert!(matches!(v, Value::Set(_)));
676    }
677
678    #[test]
679    fn test_contains_q_vector_non_integer_key_returns_false() {
680        // Regression for #206: non-integer key on a vector must return false,
681        // not throw a WrongType error.
682        assert_eq!(eval_str("(contains? [1 2 3] :a)").unwrap(), bool_v(false));
683        assert_eq!(
684            eval_str("(contains? [1 2 3] \"x\")").unwrap(),
685            bool_v(false)
686        );
687        // Integer keys still work correctly.
688        assert_eq!(eval_str("(contains? [1 2 3] 0)").unwrap(), bool_v(true));
689        assert_eq!(eval_str("(contains? [1 2 3] 9)").unwrap(), bool_v(false));
690    }
691
692    // ── set! ──────────────────────────────────────────────────────────────
693
694    #[test]
695    fn test_set_bang() {
696        let (_, mut env) = make_env();
697        eval_src("(def x 1)", &mut env).unwrap();
698        eval_src("(set! x 99)", &mut env).unwrap();
699        assert_eq!(eval_src("x", &mut env).unwrap(), long(99));
700    }
701
702    // ── throw / try / catch ───────────────────────────────────────────────
703
704    #[test]
705    fn test_throw_catch() {
706        let v = eval_str("(try (throw (ex-info \"oops\" {})) (catch Exception e (ex-message e)))")
707            .unwrap();
708        assert!(matches!(v, Value::Str(_)));
709    }
710
711    #[test]
712    fn test_try_no_throw() {
713        assert_eq!(eval_str("(try 42)").unwrap(), long(42));
714    }
715
716    #[test]
717    fn test_catch_default_catches_internal_value_errors() {
718        // Regression for #168: internal `ValueError`s (IndexOutOfBounds, WrongType)
719        // raised by core ops must be caught by the ClojureScript `:default` keyword,
720        // not just by symbol catch types.
721        for src in [
722            "(try (nth [1 2 3] 10) (catch :default e \"caught\"))",
723            "(try (aget (long-array 3) 10) (catch :default e \"caught\"))",
724            "(try (aset (long-array 3) 10 5) (catch :default e \"caught\"))",
725        ] {
726            let v = eval_str(src).unwrap();
727            assert_eq!(v, Value::string("caught"), "{src}");
728        }
729    }
730
731    #[test]
732    fn test_catch_default_binds_clean_ex_message() {
733        // The caught value is a normalized exception: `ex-message` returns the
734        // plain `ValueError` text with no `runtime error:` prefix, matching how a
735        // user `throw` / `ex-info` value behaves.
736        let v = eval_str("(try (nth [1 2 3] 10) (catch :default e (ex-message e)))").unwrap();
737        assert_eq!(v, Value::string("index out of bounds: 10 >= 3"));
738    }
739
740    #[test]
741    fn test_finally_runs_and_value_is_discarded() {
742        // finally executes for its side effect, but the `try` value is the body's.
743        let v = eval_str("(let [a (atom 0)] (try 1 (finally (reset! a 5))) @a)").unwrap();
744        assert_eq!(v, long(5));
745        assert_eq!(eval_str("(try 1 (finally 2))").unwrap(), long(1));
746    }
747
748    #[test]
749    fn test_finally_runs_after_catch() {
750        let v = eval_str(
751            "(let [a (atom 0)]
752               (try (throw (ex-info \"x\" {})) (catch Exception e :caught)
753                 (finally (reset! a 9)))
754               @a)",
755        )
756        .unwrap();
757        assert_eq!(v, long(9));
758    }
759
760    #[test]
761    fn test_finally_exception_propagates() {
762        // An exception thrown from `finally` supersedes the body's result.
763        assert!(eval_str("(try 1 (finally (throw (ex-info \"boom\" {}))))").is_err());
764    }
765
766    #[test]
767    fn test_catch_reader_conditional_type() {
768        // Regression for #210: a reader conditional in the catch type position must
769        // be resolved before matching — previously the catch clause was silently
770        // dropped and the exception escaped.
771
772        // Single-branch: #?(:rust Exception)
773        let v =
774            eval_str(r#"(try (throw (ex-info "boom" {})) (catch #?(:rust Exception) e :caught))"#)
775                .unwrap();
776        let kw_caught = Value::keyword(cljrs_value::Keyword::simple("caught"));
777        assert_eq!(v, kw_caught, "single-branch reader cond");
778
779        // Multi-branch: the :rust branch must win over :clj/:cljs/:default ordering
780        let v = eval_str(concat!(
781            "(try (throw (ex-info \"boom\" {})) ",
782            "(catch #?(:clj Throwable :cljs :default :rust Exception) e :caught))",
783        ))
784        .unwrap();
785        assert_eq!(v, kw_caught, "multi-branch reader cond");
786
787        // A reader conditional whose :rust branch resolves to :default is also a
788        // catch-all and must match any exception.
789        let v =
790            eval_str(r#"(try (throw (ex-info "boom" {})) (catch #?(:rust :default) e :caught))"#)
791                .unwrap();
792        assert_eq!(v, kw_caught, "reader cond resolves to :default");
793
794        // A reader conditional with no :rust branch must NOT catch the exception.
795        let result =
796            eval_str(r#"(try (throw (ex-info "boom" {})) (catch #?(:clj Throwable) e :caught))"#);
797        assert!(
798            result.is_err(),
799            "no matching :rust branch must let exception escape"
800        );
801    }
802
803    #[test]
804    fn test_nth_negative_index() {
805        // Negative index returns the not-found default, or throws without one.
806        assert_eq!(
807            eval_str("(= (nth [10 20 30] -1 :nf) :nf)").unwrap(),
808            bool_v(true)
809        );
810        assert!(eval_str("(nth [10 20 30] -1)").is_err());
811        assert!(eval_str("(nth '(1 2 3) -1)").is_err());
812        // Crucially, must NOT hang walking an infinite lazy seq to usize::MAX.
813        assert_eq!(
814            eval_str("(= (nth (range) -1 :nf) :nf)").unwrap(),
815            bool_v(true)
816        );
817        assert_eq!(
818            eval_str("(= (nth '(1 2 3) -1 :nf) :nf)").unwrap(),
819            bool_v(true)
820        );
821    }
822
823    // ── destructuring ─────────────────────────────────────────────────────
824
825    #[test]
826    fn test_sequential_destructure() {
827        assert_eq!(eval_str("(let* [[a b] [1 2]] (+ a b))").unwrap(), long(3));
828    }
829
830    #[test]
831    fn test_rest_destructure() {
832        let v = eval_str("(let* [[h & t] [1 2 3]] t)").unwrap();
833        assert!(matches!(v, Value::List(_)));
834        if let Value::List(l) = &v {
835            assert_eq!(l.get().count(), 2);
836        }
837    }
838
839    // ── defmacro ──────────────────────────────────────────────────────────
840
841    #[test]
842    fn test_defmacro() {
843        let (_, mut env) = make_env();
844        eval_src("(defmacro my-if [t a b] (list 'if t a b))", &mut env).unwrap();
845        assert_eq!(eval_src("(my-if true 1 2)", &mut env).unwrap(), long(1));
846        assert_eq!(eval_src("(my-if false 1 2)", &mut env).unwrap(), long(2));
847    }
848
849    // ── syntax-quote ──────────────────────────────────────────────────────
850
851    #[test]
852    fn test_syntax_quote_basic() {
853        let (_, mut env) = make_env();
854        eval_src("(def b 2)", &mut env).unwrap();
855        let v = eval_src("`(a ~b)", &mut env).unwrap();
856        assert!(matches!(v, Value::List(_)));
857        if let Value::List(l) = &v {
858            // Should be (user/a 2)
859            let items: Vec<_> = l.get().iter().cloned().collect();
860            assert_eq!(items.len(), 2);
861            assert_eq!(items[1], long(2));
862        }
863    }
864
865    // ── reader conditionals ───────────────────────────────────────────────
866
867    #[test]
868    fn test_reader_cond_rust() {
869        // :rust branch selected.
870        assert_eq!(eval_str("#?(:rust 1 :clj 2)").unwrap(), long(1));
871    }
872
873    #[test]
874    fn test_reader_cond_default() {
875        // No :rust; fall through to :default.
876        assert_eq!(eval_str("#?(:clj 2 :default 99)").unwrap(), long(99));
877    }
878
879    #[test]
880    fn test_reader_cond_splice_in_vector() {
881        assert_eq!(
882            eval_str("[1 #?@(:rust [:a :b]) 2]").unwrap(),
883            eval_str("[1 :a :b 2]").unwrap()
884        );
885    }
886
887    #[test]
888    fn test_reader_cond_splice_in_set() {
889        assert_eq!(
890            eval_str("#{1 #?@(:rust [2 3]) 4}").unwrap(),
891            eval_str("#{1 2 3 4}").unwrap()
892        );
893    }
894
895    #[test]
896    fn test_reader_cond_splice_in_map() {
897        assert_eq!(
898            eval_str("{:a 1 #?@(:rust [:b 2]) :c 3}").unwrap(),
899            eval_str("{:a 1 :b 2 :c 3}").unwrap()
900        );
901    }
902
903    #[test]
904    fn test_reader_cond_splice_in_call_args() {
905        assert_eq!(
906            eval_str("(vector 1 #?@(:rust [2 3]) 4)").unwrap(),
907            eval_str("[1 2 3 4]").unwrap()
908        );
909    }
910
911    #[test]
912    fn test_reader_cond_splice_no_match_removed() {
913        assert_eq!(
914            eval_str("[1 #?@(:clj [:a :b]) 2]").unwrap(),
915            eval_str("[1 2]").unwrap()
916        );
917    }
918
919    proptest::proptest! {
920        #![proptest_config(proptest::prelude::ProptestConfig::with_cases(48))]
921        /// Splicing `#?@(:rust mid)` into any container evaluates to the same
922        /// value as inlining `mid` literally - for vector, set, data-list, and
923        /// call arguments. Elements are distinct so set literals stay legal.
924        #[test]
925        fn prop_splice_evals_as_inline_in_every_container(
926            np in 0usize..3, nm in 0usize..3, ns in 0usize..3,
927        ) {
928            let kw_run = |start: usize, n: usize| {
929                (start..start + n)
930                    .map(|i| format!(":v{i}"))
931                    .collect::<Vec<_>>()
932                    .join(" ")
933            };
934            let p = kw_run(0, np);
935            let m = kw_run(np, nm);
936            let s = kw_run(np + nm, ns);
937            for (open, close, quote) in
938                [("[", "]", ""), ("#{", "}", ""), ("(", ")", "'"), ("(vector ", ")", "")]
939            {
940                let spliced = format!("{quote}{open}{p} #?@(:rust [{m}]) {s}{close}");
941                let inlined = format!("{quote}{open}{p} {m} {s}{close}");
942                proptest::prop_assert_eq!(
943                    eval_str(&spliced).unwrap(),
944                    eval_str(&inlined).unwrap(),
945                    "container {}{}",
946                    open,
947                    close
948                );
949            }
950        }
951    }
952
953    // ── Reader conditionals: cross-path and binding-vector properties ─────
954    //
955    // One generated position in a container or binding vector. Names are
956    // assigned by index, so generated elements are distinct and set literals
957    // stay legal.
958
959    #[derive(Clone, Debug)]
960    enum Slot {
961        /// A plain element; contributes 1.
962        Plain,
963        /// `#?(…)`; contributes 1 when the branch key matches, else 0.
964        NonSplice { matches: bool },
965        /// `#?@(…)`; contributes `n` when the branch key matches, else 0.
966        Splice { matches: bool, n: usize },
967    }
968
969    fn slot_strat() -> impl proptest::strategy::Strategy<Value = Slot> {
970        use proptest::strategy::{Just, Strategy};
971        proptest::prop_oneof![
972            Just(Slot::Plain),
973            proptest::bool::ANY.prop_map(|matches| Slot::NonSplice { matches }),
974            (proptest::bool::ANY, 0usize..3).prop_map(|(matches, n)| Slot::Splice { matches, n }),
975        ]
976    }
977
978    /// Render `slots` twice - conditionals written out, and branches inlined -
979    /// with `unit` forms per contributed position. `unit(i)` renders element
980    /// `i`; a unit of two tokens (`x0 0`) keeps pair parity even, which is what
981    /// binding vectors need.
982    fn render_slots(slots: &[Slot], unit: &dyn Fn(usize) -> String) -> (String, String) {
983        let mut next = 0usize;
984        let (mut written, mut inlined) = (Vec::new(), Vec::new());
985        for slot in slots {
986            match *slot {
987                Slot::Plain => {
988                    let u = unit(next);
989                    next += 1;
990                    written.push(u.clone());
991                    inlined.push(u);
992                }
993                Slot::NonSplice { matches } => {
994                    let u = unit(next);
995                    next += 1;
996                    let key = if matches { "rust" } else { "clj" };
997                    written.push(format!("#?(:{key} {u})"));
998                    if matches {
999                        inlined.push(u);
1000                    }
1001                }
1002                Slot::Splice { matches, n } => {
1003                    let us: Vec<String> = (0..n)
1004                        .map(|_| {
1005                            let u = unit(next);
1006                            next += 1;
1007                            u
1008                        })
1009                        .collect();
1010                    let key = if matches { "rust" } else { "clj" };
1011                    written.push(format!("#?@(:{key} [{}])", us.join(" ")));
1012                    if matches {
1013                        inlined.extend(us);
1014                    }
1015                }
1016            }
1017        }
1018        (written.join(" "), inlined.join(" "))
1019    }
1020
1021    /// Binding vectors take pairs, and a non-splicing `#?` selects exactly one
1022    /// form - so it cannot carry a `name init` pair. Only plain positions and
1023    /// `#?@` splices can appear there.
1024    fn pair_slot_strat() -> impl proptest::strategy::Strategy<Value = Slot> {
1025        use proptest::strategy::{Just, Strategy};
1026        proptest::prop_oneof![
1027            Just(Slot::Plain),
1028            (proptest::bool::ANY, 0usize..3).prop_map(|(matches, n)| Slot::Splice { matches, n }),
1029        ]
1030    }
1031
1032    fn kw_unit(i: usize) -> String {
1033        format!(":k{i}")
1034    }
1035
1036    /// `x<i> <i>` - one binding pair, so every contributed position keeps the
1037    /// vector's parity even.
1038    fn binding_unit(i: usize) -> String {
1039        format!("x{i} {i}")
1040    }
1041
1042    proptest::proptest! {
1043        #![proptest_config(proptest::prelude::ProptestConfig::with_cases(48))]
1044
1045        /// A form's meaning must not depend on which path reads it. For the same
1046        /// container, evaluating it, quoting it, and syntax-quoting it all agree
1047        /// with the hand-inlined spelling. Before this fix the quoted and
1048        /// syntax-quoted paths dropped or nil-filled splices that the evaluated
1049        /// path expanded.
1050        #[test]
1051        fn prop_splice_agrees_across_eval_quote_and_syntax_quote(
1052            slots in proptest::collection::vec(slot_strat(), 0..5),
1053        ) {
1054            let (written, inlined) = render_slots(&slots, &kw_unit);
1055            let even = inlined.split_whitespace().count().is_multiple_of(2);
1056            for (open, close) in [("[", "]"), ("#{", "}"), ("{", "}")] {
1057                if open == "{" && !even {
1058                    continue;
1059                }
1060                for prefix in ["", "'", "`"] {
1061                    let got = eval_str(&format!("{prefix}{open}{written}{close}"));
1062                    let want = eval_str(&format!("{prefix}{open}{inlined}{close}"));
1063                    proptest::prop_assert_eq!(
1064                        got.map_err(|e| e.to_string()),
1065                        want.map_err(|e| e.to_string()),
1066                        "prefix {:?} container {}{}", prefix, open, close
1067                    );
1068                }
1069            }
1070            // Data lists have no evaluated spelling; check both quoted forms.
1071            for prefix in ["'", "`"] {
1072                let got = eval_str(&format!("{prefix}({written})"));
1073                let want = eval_str(&format!("{prefix}({inlined})"));
1074                proptest::prop_assert_eq!(
1075                    got.map_err(|e| e.to_string()),
1076                    want.map_err(|e| e.to_string()),
1077                    "prefix {:?} list", prefix
1078                );
1079            }
1080        }
1081
1082        /// Every binding vector resolves conditionals: `let*`, `loop*` and
1083        /// `binding` bind exactly what the inlined spelling binds.
1084        #[test]
1085        fn prop_splice_in_binding_vectors_equals_inline(
1086            slots in proptest::collection::vec(pair_slot_strat(), 0..4),
1087        ) {
1088            let (written, inlined) = render_slots(&slots, &binding_unit);
1089            let names: Vec<String> = inlined
1090                .split_whitespace()
1091                .step_by(2)
1092                .map(str::to_string)
1093                .collect();
1094            let body = format!("[{}]", names.join(" "));
1095            for head in ["let*", "loop*"] {
1096                let got = eval_str(&format!("({head} [{written}] {body})"));
1097                let want = eval_str(&format!("({head} [{inlined}] {body})"));
1098                proptest::prop_assert_eq!(
1099                    got.map_err(|e| e.to_string()),
1100                    want.map_err(|e| e.to_string()),
1101                    "{}", head
1102                );
1103            }
1104        }
1105    }
1106
1107    #[test]
1108    fn quoted_map_splice_lands_in_key_value_positions() {
1109        assert_eq!(
1110            eval_str("'{:a 1 #?@(:rust [:b 2]) :c 3}").unwrap(),
1111            eval_str("{:a 1 :b 2 :c 3}").unwrap()
1112        );
1113    }
1114
1115    #[test]
1116    fn syntax_quoted_splice_is_expanded_not_nil() {
1117        assert_eq!(
1118            eval_str("`(1 #?@(:rust [2 3]) 4)").unwrap(),
1119            eval_str("'(1 2 3 4)").unwrap()
1120        );
1121    }
1122
1123    #[test]
1124    fn map_literal_with_only_an_unmatched_conditional_reads_as_empty() {
1125        // The written parity is odd, the expansion is empty - the reader must
1126        // defer rather than reject.
1127        assert_eq!(eval_str("{#?(:clj :a)}").unwrap(), eval_str("{}").unwrap());
1128    }
1129
1130    #[test]
1131    fn loop_binding_vector_expands_splices() {
1132        assert_eq!(eval_str("(loop* [#?@(:rust [x 1])] x)").unwrap(), long(1));
1133    }
1134
1135    // ── Error cases ───────────────────────────────────────────────────────
1136
1137    #[test]
1138    fn test_unbound_symbol() {
1139        let r = eval_str("undefined-var-xyz");
1140        assert!(matches!(r, Err(EvalError::UnboundSymbol(_))));
1141    }
1142
1143    #[test]
1144    fn test_wrong_arity() {
1145        let (_, mut env) = make_env();
1146        eval_src("(defn one-arg [x] x)", &mut env).unwrap();
1147        let r = eval_src("(one-arg 1 2)", &mut env);
1148        assert!(matches!(r, Err(EvalError::Arity { .. })));
1149    }
1150
1151    #[test]
1152    fn test_not_callable() {
1153        let r = eval_str("(42 1 2)");
1154        assert!(matches!(r, Err(EvalError::NotCallable(_))));
1155    }
1156
1157    // ── Higher-order functions (bootstrap) ────────────────────────────────
1158
1159    #[test]
1160    fn test_map_fn() {
1161        assert_eq!(
1162            eval_str("(vec (map inc [1 2 3]))").unwrap(),
1163            eval_str("[2 3 4]").unwrap()
1164        );
1165    }
1166
1167    #[test]
1168    fn test_filter_fn() {
1169        assert_eq!(
1170            eval_str("(vec (filter odd? [1 2 3 4 5]))").unwrap(),
1171            eval_str("[1 3 5]").unwrap()
1172        );
1173    }
1174
1175    #[test]
1176    fn test_reduce_fn() {
1177        assert_eq!(eval_str("(reduce + [1 2 3 4 5])").unwrap(), long(15));
1178    }
1179
1180    #[test]
1181    fn test_apply_fn() {
1182        assert_eq!(eval_str("(apply + [1 2 3])").unwrap(), long(6));
1183    }
1184
1185    #[test]
1186    fn test_atom_ops() {
1187        let (_, mut env) = make_env();
1188        eval_src("(def a (atom 0))", &mut env).unwrap();
1189        eval_src("(swap! a inc)", &mut env).unwrap();
1190        assert_eq!(eval_src("(deref a)", &mut env).unwrap(), long(1));
1191    }
1192
1193    #[test]
1194    fn test_when_macro() {
1195        assert_eq!(eval_str("(when true 42)").unwrap(), long(42));
1196        assert_eq!(eval_str("(when false 42)").unwrap(), Value::Nil);
1197    }
1198
1199    #[test]
1200    fn test_cond_macro() {
1201        assert_eq!(eval_str("(cond false 1 true 2)").unwrap(), long(2));
1202    }
1203
1204    #[test]
1205    fn test_and_or() {
1206        assert_eq!(eval_str("(and 1 2 3)").unwrap(), long(3));
1207        assert_eq!(eval_str("(and 1 false 3)").unwrap(), bool_v(false));
1208        assert_eq!(eval_str("(or false nil 42)").unwrap(), long(42));
1209        assert_eq!(eval_str("(or false nil)").unwrap(), Value::Nil);
1210    }
1211
1212    // ── Phase 5: Lazy sequences ───────────────────────────────────────────
1213
1214    #[test]
1215    fn test_lazy_range() {
1216        assert_eq!(
1217            eval_str("(= (into [] (take 5 (range))) [0 1 2 3 4])").unwrap(),
1218            bool_v(true)
1219        );
1220    }
1221
1222    #[test]
1223    fn test_lazy_range_bounded() {
1224        assert_eq!(
1225            eval_str("(= (into [] (range 3)) [0 1 2])").unwrap(),
1226            bool_v(true)
1227        );
1228    }
1229
1230    #[test]
1231    fn test_lazy_iterate() {
1232        assert_eq!(
1233            eval_str("(= (into [] (take 3 (iterate inc 0))) [0 1 2])").unwrap(),
1234            bool_v(true)
1235        );
1236    }
1237
1238    #[test]
1239    fn test_lazy_repeat() {
1240        assert_eq!(
1241            eval_str("(= (into [] (take 3 (repeat :x))) [:x :x :x])").unwrap(),
1242            bool_v(true)
1243        );
1244    }
1245
1246    #[test]
1247    fn test_lazy_cycle() {
1248        assert_eq!(
1249            eval_str("(= (into [] (take 5 (cycle [1 2]))) [1 2 1 2 1])").unwrap(),
1250            bool_v(true)
1251        );
1252    }
1253
1254    // ── Phase 5: Associative destructuring ───────────────────────────────
1255
1256    #[test]
1257    fn test_assoc_destructure() {
1258        assert_eq!(
1259            eval_str("(let [{:keys [a b]} {:a 1 :b 2}] (+ a b))").unwrap(),
1260            long(3)
1261        );
1262    }
1263
1264    #[test]
1265    fn test_assoc_destructure_or() {
1266        assert_eq!(
1267            eval_str("(let [{:keys [a b] :or {b 99}} {:a 1}] b)").unwrap(),
1268            long(99)
1269        );
1270    }
1271
1272    // ── Phase 5: letfn ───────────────────────────────────────────────────
1273
1274    #[test]
1275    fn test_letfn() {
1276        assert_eq!(
1277            eval_str("(letfn [(fact [n] (if (= n 0) 1 (* n (fact (dec n)))))] (fact 5))").unwrap(),
1278            long(120)
1279        );
1280    }
1281
1282    // letfn scope is MUTUAL, not sequential: every binding is visible to every
1283    // other one regardless of order. These pin the three-pass construction in
1284    // `eval_letfn` — a closure captures values rather than cells, so without the
1285    // pass-3 back-patch a forward reference silently sees the nil placeholder
1286    // pass 1 left behind.
1287
1288    #[test]
1289    fn test_letfn_forward_reference() {
1290        assert_eq!(
1291            eval_str("(letfn [(f [n] (g n)) (g [n] (* n 2))] (f 21))").unwrap(),
1292            long(42)
1293        );
1294    }
1295
1296    #[test]
1297    fn test_letfn_mutual_recursion() {
1298        // `my-even?` names `my-odd?` before it is built — the direction that was
1299        // broken — and `my-odd?` names `my-even?` backwards.
1300        let defs = "(letfn [(my-even? [n] (if (= n 0) true (my-odd? (dec n))))
1301                            (my-odd? [n] (if (= n 0) false (my-even? (dec n))))]";
1302        assert_eq!(
1303            eval_str(&format!("{defs} (my-even? 10))")).unwrap(),
1304            Value::Bool(true)
1305        );
1306        assert_eq!(
1307            eval_str(&format!("{defs} (my-odd? 10))")).unwrap(),
1308            Value::Bool(false)
1309        );
1310    }
1311
1312    #[test]
1313    fn test_letfn_three_way_mutual_recursion() {
1314        let defs = "(letfn [(a [n] (if (= n 0) 1 (b (dec n))))
1315                            (b [n] (if (= n 0) 2 (c (dec n))))
1316                            (c [n] (if (= n 0) 3 (a (dec n))))]";
1317        for (arg, want) in [(0, 1), (1, 2), (2, 3), (3, 1)] {
1318            assert_eq!(
1319                eval_str(&format!("{defs} (a {arg}))")).unwrap(),
1320                long(want),
1321                "(a {arg})"
1322            );
1323        }
1324    }
1325
1326    #[test]
1327    fn test_letfn_closes_over_outer_local() {
1328        assert_eq!(
1329            eval_str("(let [start 10] (letfn [(step [n] (+ n start))] (step 5)))").unwrap(),
1330            long(15)
1331        );
1332    }
1333
1334    #[test]
1335    fn test_letfn_shadows_enclosing_binding() {
1336        // The letfn binding wins over the `let` of the same name, and siblings
1337        // that name it see the fn, not the shadowed value.
1338        assert_eq!(
1339            eval_str("(let [f 1] (letfn [(g [] (f)) (f [] 7)] (g)))").unwrap(),
1340            long(7)
1341        );
1342    }
1343
1344    #[test]
1345    fn test_letfn_binding_name_must_be_a_symbol() {
1346        assert!(eval_str("(letfn [(42 [n] n)] 1)").is_err());
1347    }
1348
1349    // ── Phase 5: namespace ops ────────────────────────────────────────────
1350
1351    #[test]
1352    fn test_in_ns() {
1353        let (_, mut env) = make_env();
1354        eval_src("(in-ns 'mytest)", &mut env).unwrap();
1355        assert_eq!(env.current_ns.as_ref(), "mytest");
1356        eval_src("(in-ns 'user)", &mut env).unwrap();
1357        assert_eq!(env.current_ns.as_ref(), "user");
1358    }
1359
1360    // ── Phase 5: spit / slurp ─────────────────────────────────────────────
1361
1362    #[test]
1363    fn test_spit_slurp() {
1364        let path = std::env::temp_dir().join("cljrs_test_spit_slurp.txt");
1365        let path_str = path.to_str().unwrap();
1366        let src = format!(
1367            r#"(do (spit "{}" "hello clojurust") (slurp "{}"))"#,
1368            path_str, path_str
1369        );
1370        let result = eval_str(&src).unwrap();
1371        if let Value::Str(s) = result {
1372            assert_eq!(s.get().as_str(), "hello clojurust");
1373        } else {
1374            panic!("expected string result from slurp");
1375        }
1376        let _ = std::fs::remove_file(path);
1377    }
1378
1379    // ── Phase 5: update-in ───────────────────────────────────────────────
1380
1381    #[test]
1382    fn test_update_in() {
1383        assert_eq!(
1384            eval_str("(= (update-in {:a {:b 1}} [:a :b] inc) {:a {:b 2}})").unwrap(),
1385            bool_v(true)
1386        );
1387    }
1388
1389    // ── Phase 5: if-let / when-let ────────────────────────────────────────
1390
1391    #[test]
1392    fn test_if_let_truthy() {
1393        assert_eq!(eval_str("(if-let [x 42] x :nope)").unwrap(), long(42));
1394    }
1395
1396    #[test]
1397    fn test_if_let_falsy() {
1398        assert_eq!(
1399            eval_str("(if-let [x nil] x :nope)").unwrap(),
1400            eval_str(":nope").unwrap()
1401        );
1402    }
1403
1404    #[test]
1405    fn test_when_let_truthy() {
1406        assert_eq!(eval_str("(when-let [x 7] (* x 2))").unwrap(), long(14));
1407    }
1408
1409    #[test]
1410    fn test_when_let_falsy() {
1411        assert_eq!(eval_str("(when-let [x nil] 99)").unwrap(), Value::Nil);
1412    }
1413
1414    // ── Phase 5: math functions ───────────────────────────────────────────
1415
1416    #[test]
1417    fn test_math_trig() {
1418        // sin(0) = 0, cos(0) = 1
1419        assert_eq!(eval_str("(Math/sin 0)").unwrap(), Value::Double(0.0));
1420        assert_eq!(eval_str("(Math/cos 0)").unwrap(), Value::Double(1.0));
1421    }
1422
1423    #[test]
1424    fn test_math_constants() {
1425        assert!(
1426            matches!(eval_str("Math/PI").unwrap(), Value::Double(v) if (v - std::f64::consts::PI).abs() < 1e-10)
1427        );
1428        assert!(
1429            matches!(eval_str("Math/E").unwrap(), Value::Double(v) if (v - std::f64::consts::E).abs() < 1e-10)
1430        );
1431    }
1432
1433    #[test]
1434    fn test_math_log_exp() {
1435        // exp(0) = 1, log(1) = 0
1436        assert_eq!(eval_str("(Math/exp 0)").unwrap(), Value::Double(1.0));
1437        assert_eq!(eval_str("(Math/log 1)").unwrap(), Value::Double(0.0));
1438    }
1439
1440    // ── Phase 6: Protocols & Multimethods ─────────────────────────────────
1441
1442    #[test]
1443    fn test_defprotocol() {
1444        // Defining a protocol creates a callable ProtocolFn that errors without impl.
1445        let result = eval_str(
1446            r#"
1447            (defprotocol Greet
1448              (greet [this]))
1449            (greet "hello")
1450            "#,
1451        );
1452        assert!(result.is_err());
1453        let msg = result.unwrap_err().to_string();
1454        assert!(msg.contains("No implementation"), "got: {msg}");
1455    }
1456
1457    #[test]
1458    fn test_extend_type() {
1459        let result = eval_str(
1460            r#"
1461            (defprotocol Greet
1462              (greet [this]))
1463            (extend-type String
1464              Greet
1465              (greet [this] (str "Hello, " this "!")))
1466            (greet "world")
1467            "#,
1468        )
1469        .unwrap();
1470        assert_eq!(result, Value::string("Hello, world!"));
1471    }
1472
1473    #[test]
1474    fn test_protocol_dispatch() {
1475        let result = eval_str(
1476            r#"
1477            (defprotocol Describable
1478              (describe [this]))
1479            (extend-type String
1480              Describable
1481              (describe [this] (str "string:" this)))
1482            (extend-type Long
1483              Describable
1484              (describe [this] (str "long:" this)))
1485            [(describe "hi") (describe 42)]
1486            "#,
1487        )
1488        .unwrap();
1489        assert!(matches!(result, Value::Vector(_)));
1490        let s = format!("{}", result);
1491        assert!(s.contains("string:hi"), "got: {s}");
1492        assert!(s.contains("long:42"), "got: {s}");
1493    }
1494
1495    #[test]
1496    fn test_extend_protocol() {
1497        let result = eval_str(
1498            r#"
1499            (defprotocol Showable
1500              (show [this]))
1501            (extend-protocol Showable
1502              String
1503              (show [this] (str "S:" this))
1504              Long
1505              (show [this] (str "L:" this)))
1506            [(show "x") (show 7)]
1507            "#,
1508        )
1509        .unwrap();
1510        let s = format!("{}", result);
1511        assert!(s.contains("S:x"), "got: {s}");
1512        assert!(s.contains("L:7"), "got: {s}");
1513    }
1514
1515    #[test]
1516    fn test_extend_via_metadata() {
1517        // `:extend-via-metadata true` lets an instance implement a protocol by
1518        // carrying the impl fn in its own metadata, keyed by the protocol
1519        // method's fully-qualified symbol (matching real Clojure's
1520        // `MethodImplCache` dispatch, which looks up `(.sym cache)` in
1521        // `(meta x)`) — no `extend-type`/`extend-protocol` needed. Idiomatic
1522        // usage produces that qualified symbol via syntax-quote.
1523        let result = eval_str(
1524            r#"
1525            (defprotocol IRender
1526              :extend-via-metadata true
1527              (create-element [this tag-name]))
1528            (def renderer (with-meta {} {`create-element (fn [this tag-name] (str "made-" tag-name))}))
1529            (create-element renderer "div")
1530            "#,
1531        )
1532        .unwrap();
1533        assert_eq!(result, Value::string("made-div"));
1534    }
1535
1536    #[test]
1537    fn test_extend_via_metadata_falls_back_to_type_tag() {
1538        // Metadata impls take priority, but a value without metadata still
1539        // dispatches on its type tag as usual.
1540        let result = eval_str(
1541            r#"
1542            (defprotocol IRender
1543              :extend-via-metadata true
1544              (create-element [this tag-name]))
1545            (extend-type Map
1546              IRender
1547              (create-element [this tag-name] (str "type-tag-" tag-name)))
1548            [(create-element {} "span")
1549             (create-element (with-meta {} {`create-element (fn [this tag-name] (str "meta-" tag-name))}) "div")]
1550            "#,
1551        )
1552        .unwrap();
1553        let s = format!("{}", result);
1554        assert!(s.contains("type-tag-span"), "got: {s}");
1555        assert!(s.contains("meta-div"), "got: {s}");
1556    }
1557
1558    #[test]
1559    fn test_extend_via_metadata_cross_ns() {
1560        // Mirrors Replicant's mutation_log fake renderer: `IRender` is
1561        // defined in `replicant.core`, and a test namespace `:refer`s the
1562        // method and implements it purely via metadata (no `extend-type`).
1563        // Syntax-quoting `create-element` there must resolve to the
1564        // protocol's home namespace (`replicant.core/create-element`), which
1565        // is exactly the key the dispatcher looks up.
1566        let dir = temp_ns_dir("extend_via_metadata_cross_ns");
1567        std::fs::create_dir_all(dir.join("replicant")).unwrap();
1568        std::fs::write(
1569            dir.join("replicant").join("core.cljrs"),
1570            r#"(ns replicant.core)
1571               (defprotocol IRender
1572                 :extend-via-metadata true
1573                 (create-element [this tag-name]))"#,
1574        )
1575        .unwrap();
1576        let (_, mut env) = make_env_with_paths(vec![dir]);
1577        let result = eval_src(
1578            r#"
1579            (ns mutation-log-test
1580              (:require [replicant.core :refer [create-element]]))
1581            (def renderer (with-meta {} {`create-element (fn [this tag-name] (str "made-" tag-name))}))
1582            (create-element renderer "div")
1583            "#,
1584            &mut env,
1585        )
1586        .unwrap();
1587        assert_eq!(result, Value::string("made-div"));
1588    }
1589
1590    #[test]
1591    fn test_extend_via_metadata_cross_ns_via_alias() {
1592        // The `:refer` case above never touches the buggy path: a `:refer`d
1593        // bare symbol resolves through `lookup_var_in_ns`, which was always
1594        // correct. Real usage syntax-quotes an *aliased* symbol instead —
1595        // `` `p/attached? `` — which used to hit `qualify_symbol`'s "already
1596        // has a slash, keep as-is" branch and leak the alias text (`p/...`)
1597        // into the produced symbol instead of resolving it to the protocol's
1598        // home namespace (`replicant.protocols/...`), so the metadata key
1599        // never matched.
1600        let dir = temp_ns_dir("extend_via_metadata_cross_ns_via_alias");
1601        std::fs::create_dir_all(dir.join("replicant")).unwrap();
1602        std::fs::write(
1603            dir.join("replicant").join("protocols.cljrs"),
1604            r#"(ns replicant.protocols)
1605               (defprotocol IRender
1606                 :extend-via-metadata true
1607                 (attached? [this el]))"#,
1608        )
1609        .unwrap();
1610        let (_, mut env) = make_env_with_paths(vec![dir]);
1611        let result = eval_src(
1612            r#"
1613            (ns mutation-log-test
1614              (:require [replicant.protocols :as p]))
1615            (def r (with-meta {:log []} {`p/attached? (fn [_ el] el)}))
1616            (p/attached? r :el)
1617            "#,
1618            &mut env,
1619        )
1620        .unwrap();
1621        assert_eq!(result, Value::keyword(Keyword::simple("el")));
1622    }
1623
1624    #[test]
1625    fn test_satisfies() {
1626        let result = eval_str(
1627            r#"
1628            (defprotocol Animal
1629              (speak [this]))
1630            (extend-type String
1631              Animal
1632              (speak [this] this))
1633            [(satisfies? Animal "dog") (satisfies? Animal 42)]
1634            "#,
1635        )
1636        .unwrap();
1637        let s = format!("{}", result);
1638        assert!(s.contains("true"), "got: {s}");
1639        assert!(s.contains("false"), "got: {s}");
1640    }
1641
1642    #[test]
1643    fn test_defmulti_defmethod() {
1644        // Note: fn param destructuring not yet supported; use explicit map lookups.
1645        let result = eval_str(
1646            r#"
1647            (defmulti area :shape)
1648            (defmethod area :circle [m] (* 3 (:r m) (:r m)))
1649            (defmethod area :rectangle [m] (* (:w m) (:h m)))
1650            [(area {:shape :circle :r 2}) (area {:shape :rectangle :w 3 :h 4})]
1651            "#,
1652        )
1653        .unwrap();
1654        let s = format!("{}", result);
1655        // circle: 3*2*2=12, rectangle: 3*4=12
1656        assert!(s.contains("12"), "got: {s}");
1657    }
1658
1659    #[test]
1660    fn test_default_dispatch() {
1661        let result = eval_str(
1662            r#"
1663            (defmulti classify :kind)
1664            (defmethod classify :default [x] :unknown)
1665            (defmethod classify :cat [x] :meow)
1666            [(classify {:kind :dog}) (classify {:kind :cat})]
1667            "#,
1668        )
1669        .unwrap();
1670        let s = format!("{}", result);
1671        assert!(s.contains(":unknown"), "got: {s}");
1672        assert!(s.contains(":meow"), "got: {s}");
1673    }
1674
1675    #[test]
1676    fn test_prefer_method() {
1677        // prefer-method shouldn't error; just records preference
1678        let result = eval_str(
1679            r#"
1680            (defmulti foo identity)
1681            (defmethod foo :a [x] 1)
1682            (prefer-method foo :a :b)
1683            (foo :a)
1684            "#,
1685        )
1686        .unwrap();
1687        assert_eq!(result, Value::Long(1));
1688    }
1689
1690    #[test]
1691    fn test_remove_method() {
1692        let result = eval_str(
1693            r#"
1694            (defmulti bar identity)
1695            (defmethod bar :x [_] 99)
1696            (remove-method bar :x)
1697            (bar :x)
1698            "#,
1699        );
1700        assert!(result.is_err());
1701        let msg = result.unwrap_err().to_string();
1702        assert!(msg.contains("No method"), "got: {msg}");
1703    }
1704
1705    // ── Phase 7: Concurrency primitives ──────────────────────────────────────
1706
1707    #[test]
1708    fn test_compare_and_set() {
1709        let result = eval_str(
1710            r#"
1711            (let [a (atom 10)]
1712              [(compare-and-set! a 10 20)   ; succeeds: 10 == 10
1713               (compare-and-set! a 10 30)   ; fails:    20 != 10
1714               @a])
1715            "#,
1716        )
1717        .unwrap();
1718        let s = format!("{}", result);
1719        assert!(s.contains("true"), "got: {s}");
1720        assert!(s.contains("false"), "got: {s}");
1721        assert!(s.contains("20"), "got: {s}");
1722    }
1723
1724    #[test]
1725    fn test_volatile() {
1726        let result = eval_str(
1727            r#"
1728            (let [v (volatile! 1)]
1729              (vreset! v 2)
1730              (vswap! v + 10)
1731              @v)
1732            "#,
1733        )
1734        .unwrap();
1735        assert_eq!(result, Value::Long(12));
1736    }
1737
1738    #[test]
1739    fn test_delay() {
1740        // Body should not be evaluated until forced.
1741        let result = eval_str(
1742            r#"
1743            (let [calls (atom 0)
1744                  d (delay (swap! calls inc) 42)]
1745              [@calls (force d) @calls (force d) @calls])
1746            "#,
1747        )
1748        .unwrap();
1749        let s = format!("{}", result);
1750        // calls starts at 0, force evaluates body once (returns 42), second force uses cache
1751        // s = [0 42 1 42 1]
1752        assert!(s.starts_with("[0 42 1 42 1]"), "got: {s}");
1753    }
1754
1755    #[test]
1756    fn test_realized() {
1757        let result = eval_str(
1758            r#"
1759            (let [d (delay 99)]
1760              [(realized? d) (force d) (realized? d)])
1761            "#,
1762        )
1763        .unwrap();
1764        let s = format!("{}", result);
1765        assert!(s.starts_with("[false 99 true]"), "got: {s}");
1766    }
1767
1768    #[test]
1769    fn test_promise() {
1770        let result = eval_str(
1771            r#"
1772            (let [p (promise)]
1773              (deliver p 42)
1774              (deliver p 99)  ; second deliver is ignored
1775              @p)
1776            "#,
1777        )
1778        .unwrap();
1779        assert_eq!(result, Value::Long(42));
1780    }
1781
1782    #[test]
1783    #[ignore = "clojure.core/future is undefined by design: @f deadlocks the one \
1784                thread the task needs (GcPtr: !Send). cljrs.core.experimental/future \
1785                is the cooperative stand-in, read with (await f)"]
1786    fn test_future() {
1787        let result = eval_str(
1788            r#"
1789            (let [f (future (+ 1 2))]
1790              @f)
1791            "#,
1792        )
1793        .unwrap();
1794        assert_eq!(result, Value::Long(3));
1795    }
1796
1797    #[test]
1798    #[ignore = "agent not yet implemented (Phase A1 — GcPtr: !Send)"]
1799    fn test_agent_send() {
1800        let result = eval_str(
1801            r#"
1802            (let [a (agent 0)]
1803              (send a + 1)
1804              (send a + 2)
1805              (await-agent a)
1806              @a)
1807            "#,
1808        )
1809        .unwrap();
1810        assert_eq!(result, Value::Long(3));
1811    }
1812
1813    #[test]
1814    #[ignore = "agent not yet implemented (Phase A1 — GcPtr: !Send)"]
1815    fn test_agent_error_restart() {
1816        let result = eval_str(
1817            r#"
1818            (let [a (agent 10)]
1819              (send a (fn [_] (throw (ex-info "boom" {}))))
1820              (await-agent a)
1821              (let [err (agent-error a)]
1822                (restart-agent a 99)
1823                [err @a]))
1824            "#,
1825        )
1826        .unwrap();
1827        let s = format!("{}", result);
1828        // err should be a string containing "boom", @a should be 99
1829        assert!(s.contains("boom"), "got: {s}");
1830        assert!(s.contains("99"), "got: {s}");
1831    }
1832
1833    #[test]
1834    fn test_defrecord_basic() {
1835        // Constructor and field access via keyword.
1836        let result = eval_str(
1837            r#"
1838            (defrecord Point [x y])
1839            (let [p (->Point 3 4)]
1840              [(:x p) (:y p)])
1841            "#,
1842        )
1843        .unwrap();
1844        assert_eq!(result.to_string(), "[3 4]");
1845    }
1846
1847    #[test]
1848    fn test_defrecord_map_constructor() {
1849        let result = eval_str(
1850            r#"
1851            (defrecord Color [r g b])
1852            (let [c (map->Color {:r 255 :g 128 :b 0})]
1853              [(:r c) (:g c) (:b c)])
1854            "#,
1855        )
1856        .unwrap();
1857        assert_eq!(result.to_string(), "[255 128 0]");
1858    }
1859
1860    #[test]
1861    fn test_defrecord_assoc() {
1862        // assoc on a record returns a new record of the same type.
1863        let result = eval_str(
1864            r#"
1865            (defrecord Pt [x y])
1866            (let [p (->Pt 1 2)
1867                  q (assoc p :x 99)]
1868              [(:x q) (:y q) (record? q)])
1869            "#,
1870        )
1871        .unwrap();
1872        assert_eq!(result.to_string(), "[99 2 true]");
1873    }
1874
1875    #[test]
1876    fn test_defrecord_with_protocol() {
1877        let result = eval_str(
1878            r#"
1879            (defprotocol IShape
1880              (area [this]))
1881            (defrecord Circle [radius]
1882              IShape
1883              (area [this] (* 3 (:radius this) (:radius this))))
1884            (let [c (->Circle 5)]
1885              (area c))
1886            "#,
1887        )
1888        .unwrap();
1889        assert_eq!(result, cljrs_value::Value::Long(75));
1890    }
1891
1892    #[test]
1893    fn test_instance_q() {
1894        let result = eval_str(
1895            r#"
1896            (defrecord Dog [name])
1897            (let [d (->Dog "Rex")]
1898              [(instance? Dog d) (instance? Dog 42)])
1899            "#,
1900        )
1901        .unwrap();
1902        assert_eq!(result.to_string(), "[true false]");
1903    }
1904
1905    #[test]
1906    fn test_reify_basic() {
1907        let result = eval_str(
1908            r#"
1909            (defprotocol IGreet
1910              (greet [this name]))
1911            (let [greeter (reify IGreet
1912                            (greet [this name] (str "Hello, " name "!")))]
1913              (greet greeter "World"))
1914            "#,
1915        )
1916        .unwrap();
1917        assert_eq!(result.to_string(), "\"Hello, World!\"");
1918    }
1919
1920    // ── require / load-file ───────────────────────────────────────────────
1921
1922    fn temp_ns_dir(test_name: &str) -> std::path::PathBuf {
1923        let dir = std::env::temp_dir().join(format!("cljrs_test_{test_name}"));
1924        let _ = std::fs::remove_dir_all(&dir);
1925        std::fs::create_dir_all(&dir).unwrap();
1926        dir
1927    }
1928
1929    fn make_env_with_paths(paths: Vec<std::path::PathBuf>) -> (Arc<GlobalEnv>, Env) {
1930        let globals = crate::Runtime::builder()
1931            .execution_mode(crate::ExecutionMode::TreeWalk)
1932            .eager_clojure_test(true)
1933            .source_paths(paths)
1934            .build()
1935            .expect("runtime")
1936            .into_globals();
1937        let env = Env::new(globals.clone(), "user");
1938        (globals, env)
1939    }
1940
1941    #[test]
1942    fn test_require_as() {
1943        let dir = temp_ns_dir("require_as");
1944        std::fs::write(
1945            dir.join("mylib.cljrs"),
1946            "(ns mylib) (defn greet [n] (str \"hello \" n))",
1947        )
1948        .unwrap();
1949        let (_, mut env) = make_env_with_paths(vec![dir]);
1950        let result = eval_src("(require '[mylib :as ml]) (ml/greet \"world\")", &mut env).unwrap();
1951        assert_eq!(result.to_string(), "\"hello world\"");
1952    }
1953
1954    #[test]
1955    fn test_require_refer() {
1956        let dir = temp_ns_dir("require_refer");
1957        std::fs::write(
1958            dir.join("myutil.cljrs"),
1959            "(ns myutil) (defn twice [x] (* 2 x))",
1960        )
1961        .unwrap();
1962        let (_, mut env) = make_env_with_paths(vec![dir]);
1963        let result = eval_src("(require '[myutil :refer [twice]]) (twice 21)", &mut env).unwrap();
1964        assert_eq!(result, Value::Long(42));
1965    }
1966
1967    #[test]
1968    fn test_require_refer_all() {
1969        let dir = temp_ns_dir("require_refer_all");
1970        std::fs::write(
1971            dir.join("mymath.cljrs"),
1972            "(ns mymath) (defn square [x] (* x x))",
1973        )
1974        .unwrap();
1975        let (_, mut env) = make_env_with_paths(vec![dir]);
1976        let result = eval_src("(require '[mymath :refer :all]) (square 7)", &mut env).unwrap();
1977        assert_eq!(result, Value::Long(49));
1978    }
1979
1980    #[test]
1981    fn test_ns_require_clause() {
1982        let dir = temp_ns_dir("ns_require");
1983        std::fs::write(
1984            dir.join("greeter.cljrs"),
1985            "(ns greeter) (defn hi [n] (str \"Hi \" n))",
1986        )
1987        .unwrap();
1988        let (_, mut env) = make_env_with_paths(vec![dir]);
1989        let result = eval_src(
1990            "(ns myapp (:require [greeter :as g])) (g/hi \"Alice\")",
1991            &mut env,
1992        )
1993        .unwrap();
1994        assert_eq!(result.to_string(), "\"Hi Alice\"");
1995    }
1996
1997    #[test]
1998    fn test_var_quote_alias_resolution() {
1999        // #'alias/sym must resolve the alias to the full namespace, just like
2000        // a regular function call does (issue #187).
2001        let dir = temp_ns_dir("var_quote_alias");
2002        // lib.core maps to lib/core.cljrs on the source path.
2003        std::fs::create_dir_all(dir.join("lib")).unwrap();
2004        std::fs::write(
2005            dir.join("lib/core.cljrs"),
2006            "(ns lib.core) (defn public [x] (* x 2))",
2007        )
2008        .unwrap();
2009        let (_, mut env) = make_env_with_paths(vec![dir]);
2010        // Regular call via alias must work first.
2011        let call_result = eval_src("(require '[lib.core :as l]) (l/public 21)", &mut env).unwrap();
2012        assert_eq!(call_result, Value::Long(42));
2013        // #'alias/sym reader form.
2014        let var_result = eval_src("#'l/public", &mut env).unwrap();
2015        assert!(
2016            matches!(var_result, Value::Var(_)),
2017            "expected Var, got {var_result:?}"
2018        );
2019        assert_eq!(var_result.to_string(), "#'lib.core/public");
2020        // (var alias/sym) special form must also resolve the alias.
2021        let var_special = eval_src("(var l/public)", &mut env).unwrap();
2022        assert_eq!(var_special.to_string(), "#'lib.core/public");
2023    }
2024
2025    #[test]
2026    fn test_require_idempotent() {
2027        let dir = temp_ns_dir("require_idempotent");
2028        // File has a side effect tracked via an atom
2029        std::fs::write(
2030            dir.join("counter.cljrs"),
2031            "(ns counter) (def loaded-count (atom 0)) (swap! loaded-count inc)",
2032        )
2033        .unwrap();
2034        let (globals, mut env) = make_env_with_paths(vec![dir]);
2035        eval_src("(require 'counter)", &mut env).unwrap();
2036        eval_src("(require 'counter)", &mut env).unwrap();
2037        // The atom should have been incremented only once.
2038        let count = globals.lookup_in_ns("counter", "loaded-count").unwrap();
2039        if let Value::Atom(a) = count {
2040            assert_eq!(a.get().deref(), Value::Long(1));
2041        } else {
2042            panic!("expected atom");
2043        }
2044    }
2045
2046    #[test]
2047    fn test_require_not_found() {
2048        let (_, mut env) = make_env_with_paths(vec![]);
2049        let err = eval_src("(require 'nonexistent.ns)", &mut env).unwrap_err();
2050        let msg = format!("{err:?}");
2051        assert!(msg.contains("nonexistent.ns"), "unexpected error: {msg}");
2052    }
2053
2054    #[test]
2055    fn test_require_circular() {
2056        let dir = temp_ns_dir("require_circular");
2057        // a requires b, b requires a
2058        std::fs::write(dir.join("cira.cljrs"), "(ns cira (:require [cirb]))").unwrap();
2059        std::fs::write(dir.join("cirb.cljrs"), "(ns cirb (:require [cira]))").unwrap();
2060        let (_, mut env) = make_env_with_paths(vec![dir]);
2061        let err = eval_src("(require 'cira)", &mut env).unwrap_err();
2062        let msg = format!("{err:?}");
2063        assert!(
2064            msg.contains("circular"),
2065            "expected circular error, got: {msg}"
2066        );
2067    }
2068
2069    #[test]
2070    fn test_load_file() {
2071        let dir = temp_ns_dir("load_file");
2072        let path = dir.join("script.cljrs");
2073        std::fs::write(&path, "(+ 1 2)").unwrap();
2074        let (_, mut env) = make_env_with_paths(vec![]);
2075        let result = eval_src(&format!("(load-file \"{}\")", path.display()), &mut env).unwrap();
2076        assert_eq!(result, Value::Long(3));
2077    }
2078
2079    // ── *ns* and namespace reflection ─────────────────────────────────────────
2080
2081    #[test]
2082    fn test_star_ns_initial() {
2083        // After standard_env(), *ns* should be the user namespace.
2084        let (_, mut env) = make_env();
2085        let v = eval_src("*ns*", &mut env).unwrap();
2086        match v {
2087            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "user"),
2088            other => panic!("expected Namespace, got {other:?}"),
2089        }
2090    }
2091
2092    #[test]
2093    fn test_star_ns_after_in_ns() {
2094        let (_, mut env) = make_env();
2095        eval_src("(in-ns 'myns)", &mut env).unwrap();
2096        let v = eval_src("*ns*", &mut env).unwrap();
2097        match v {
2098            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "myns"),
2099            other => panic!("expected Namespace, got {other:?}"),
2100        }
2101    }
2102
2103    #[test]
2104    fn test_star_ns_after_ns_form() {
2105        let (_, mut env) = make_env();
2106        eval_src("(ns mytest.ns)", &mut env).unwrap();
2107        let v = eval_src("*ns*", &mut env).unwrap();
2108        match v {
2109            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "mytest.ns"),
2110            other => panic!("expected Namespace, got {other:?}"),
2111        }
2112    }
2113
2114    #[test]
2115    fn test_ns_name() {
2116        let (_, mut env) = make_env();
2117        let v = eval_src("(ns-name *ns*)", &mut env).unwrap();
2118        match v {
2119            Value::Symbol(s) => assert_eq!(s.get().name.as_ref(), "user"),
2120            other => panic!("expected Symbol, got {other:?}"),
2121        }
2122    }
2123
2124    #[test]
2125    fn test_find_ns() {
2126        let (_, mut env) = make_env();
2127        // known ns
2128        let v = eval_src("(find-ns 'user)", &mut env).unwrap();
2129        assert!(matches!(v, Value::Namespace(_)));
2130        // unknown ns
2131        let v2 = eval_src("(find-ns 'nonexistent)", &mut env).unwrap();
2132        assert_eq!(v2, Value::Nil);
2133    }
2134
2135    #[test]
2136    fn test_all_ns() {
2137        let (_, mut env) = make_env();
2138        let v = eval_src("(all-ns)", &mut env).unwrap();
2139        // Should be a list containing at least user and clojure.core
2140        let names: Vec<String> = match &v {
2141            Value::List(l) => l
2142                .get()
2143                .iter()
2144                .filter_map(|ns| match ns {
2145                    Value::Namespace(n) => Some(n.get().name.as_ref().to_string()),
2146                    _ => None,
2147                })
2148                .collect(),
2149            other => panic!("expected list, got {other:?}"),
2150        };
2151        assert!(names.contains(&"user".to_string()));
2152        assert!(names.contains(&"clojure.core".to_string()));
2153    }
2154
2155    #[test]
2156    fn test_ns_interns() {
2157        let (_, mut env) = make_env();
2158        eval_src("(def my-test-var 42)", &mut env).unwrap();
2159        let v = eval_src("(ns-interns *ns*)", &mut env).unwrap();
2160        let Value::Map(m) = v else {
2161            panic!("expected map")
2162        };
2163        // The map should contain 'my-test-var
2164        let sym = Value::symbol(cljrs_value::Symbol::simple("my-test-var"));
2165        assert!(m.get(&sym).is_some());
2166    }
2167
2168    #[test]
2169    fn test_create_ns() {
2170        let (_, mut env) = make_env();
2171        let v = eval_src("(create-ns 'fresh.ns)", &mut env).unwrap();
2172        match v {
2173            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "fresh.ns"),
2174            other => panic!("expected Namespace, got {other:?}"),
2175        }
2176        // find-ns should now find it
2177        let v2 = eval_src("(find-ns 'fresh.ns)", &mut env).unwrap();
2178        assert!(matches!(v2, Value::Namespace(_)));
2179    }
2180
2181    // ── Dynamic variables (Phase 9) ───────────────────────────────────────────
2182
2183    #[test]
2184    fn test_dynamic_var_basic() {
2185        let (globals, mut env) = make_env();
2186        let result = eval_src("(def ^:dynamic *x* 10) (binding [*x* 42] *x*)", &mut env).unwrap();
2187        assert_eq!(result, Value::Long(42));
2188        // verify root is still bound
2189        let root = globals.lookup_in_ns("user", "*x*");
2190        assert_eq!(root, Some(Value::Long(10)));
2191    }
2192
2193    #[test]
2194    fn test_dynamic_var_restore() {
2195        let (_globals, mut env) = make_env();
2196        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2197        eval_src("(binding [*x* 42] *x*)", &mut env).unwrap();
2198        // After binding block, value restored to root
2199        let val = eval_src("*x*", &mut env).unwrap();
2200        assert_eq!(val, Value::Long(10));
2201    }
2202
2203    #[test]
2204    fn test_dynamic_var_nested() {
2205        let (_, mut env) = make_env();
2206        eval_src("(def ^:dynamic *x* 1)", &mut env).unwrap();
2207        let result = eval_src("(binding [*x* 2] (binding [*x* 3] *x*))", &mut env).unwrap();
2208        assert_eq!(result, Value::Long(3));
2209        // After both blocks
2210        let val = eval_src("*x*", &mut env).unwrap();
2211        assert_eq!(val, Value::Long(1));
2212    }
2213
2214    #[test]
2215    fn test_dynamic_var_unaffected() {
2216        let (_, mut env) = make_env();
2217        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2218        eval_src("(def y 99)", &mut env).unwrap();
2219        eval_src("(binding [*x* 42] *x*)", &mut env).unwrap();
2220        // non-dynamic var y is unchanged
2221        let val = eval_src("y", &mut env).unwrap();
2222        assert_eq!(val, Value::Long(99));
2223    }
2224
2225    #[test]
2226    #[ignore = "clojure.core/future is undefined by design; the experimental \
2227                stand-in does not convey dynamic bindings to its task"]
2228    fn test_binding_conveyance() {
2229        let (_, mut env) = make_env();
2230        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2231        let result = eval_src("(binding [*x* 42] @(future *x*))", &mut env).unwrap();
2232        assert_eq!(result, Value::Long(42));
2233    }
2234
2235    #[test]
2236    fn test_var_set_in_binding() {
2237        let (_, mut env) = make_env();
2238        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2239        // set! inside binding sets thread-local
2240        let inside = eval_src("(binding [*x* 1] (set! *x* 2) *x*)", &mut env).unwrap();
2241        assert_eq!(inside, Value::Long(2));
2242        // root still 10
2243        let root = eval_src("*x*", &mut env).unwrap();
2244        assert_eq!(root, Value::Long(10));
2245    }
2246
2247    #[test]
2248    fn test_with_bindings_star() {
2249        let (_, mut env) = make_env();
2250        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2251        let result = eval_src("(with-bindings* {#'*x* 99} (fn [] *x*))", &mut env).unwrap();
2252        assert_eq!(result, Value::Long(99));
2253    }
2254
2255    #[test]
2256    fn test_binding_fully_qualified_cross_ns_dynamic_var() {
2257        let (_, mut env) = make_env();
2258        let result = eval_src(
2259            r#"
2260            (ns other.ns)
2261            (def ^:dynamic *dispatch* nil)
2262            (ns user)
2263            (binding [other.ns/*dispatch* (fn [x] x)]
2264              (other.ns/*dispatch* 42))
2265            "#,
2266            &mut env,
2267        )
2268        .unwrap();
2269        assert_eq!(result, Value::Long(42));
2270    }
2271
2272    #[test]
2273    fn test_binding_aliased_cross_ns_dynamic_var() {
2274        // (binding [alias/*var* v] ...) must resolve `alias` through the
2275        // current ns's `:require :as` aliases, exactly like ordinary
2276        // qualified-symbol lookup — this is how Replicant's public
2277        // `set-dispatch!`/life-cycle dispatch binds `*dispatch*` across
2278        // namespaces.
2279        let dir = temp_ns_dir("binding_aliased_cross_ns_dynamic_var");
2280        std::fs::create_dir_all(dir.join("replicant")).unwrap();
2281        std::fs::write(
2282            dir.join("replicant").join("core.cljrs"),
2283            r#"(ns replicant.core)
2284               (def ^:dynamic *dispatch* nil)
2285               (defn call-dispatch [x] (*dispatch* x))"#,
2286        )
2287        .unwrap();
2288        let (_, mut env) = make_env_with_paths(vec![dir]);
2289        let result = eval_src(
2290            r#"
2291            (ns life-cycle-test
2292              (:require [replicant.core :as r]))
2293            (binding [r/*dispatch* (fn [x] x)]
2294              (r/call-dispatch 42))
2295            "#,
2296            &mut env,
2297        )
2298        .unwrap();
2299        assert_eq!(result, Value::Long(42));
2300    }
2301
2302    #[test]
2303    fn test_meta_on_var() {
2304        let (_, mut env) = make_env();
2305        eval_src("(def ^:dynamic *x* 1)", &mut env).unwrap();
2306        let m = eval_src("(meta #'*x*)", &mut env).unwrap();
2307        // meta should be {:dynamic true}
2308        if let Value::Map(mv) = &m {
2309            let kw = Value::keyword(cljrs_value::Keyword::parse("dynamic"));
2310            assert_eq!(mv.get(&kw), Some(Value::Bool(true)));
2311        } else {
2312            panic!("expected map, got {m:?}");
2313        }
2314    }
2315
2316    #[test]
2317    fn test_bound_pred() {
2318        let (_, mut env) = make_env();
2319        eval_src("(def ^:dynamic *x* 1)", &mut env).unwrap();
2320        let t = eval_src("(bound? #'*x*)", &mut env).unwrap();
2321        assert_eq!(t, Value::Bool(true));
2322    }
2323
2324    #[test]
2325    fn test_alter_var_root() {
2326        let (_, mut env) = make_env();
2327        eval_src("(def x 1)", &mut env).unwrap();
2328        eval_src("(alter-var-root #'x inc)", &mut env).unwrap();
2329        let val = eval_src("x", &mut env).unwrap();
2330        assert_eq!(val, Value::Long(2));
2331    }
2332
2333    // ── clojure.test ─────────────────────────────────────────────────────────
2334
2335    #[test]
2336    fn test_clojure_test_is_pass() {
2337        // (is expr) returns true on a passing assertion.
2338        let (_, mut env) = make_env();
2339        eval_src(
2340            "(require '[clojure.test :refer [is deftest run-tests]])",
2341            &mut env,
2342        )
2343        .unwrap();
2344        let v = eval_src("(is (= 1 1))", &mut env).unwrap();
2345        assert_eq!(v, Value::Bool(true));
2346    }
2347
2348    #[test]
2349    fn test_clojure_test_is_fail() {
2350        // (is expr) returns false on a failing assertion.
2351        let (_, mut env) = make_env();
2352        eval_src("(require '[clojure.test :refer [is]])", &mut env).unwrap();
2353        let v = eval_src("(is (= 1 2))", &mut env).unwrap();
2354        assert_eq!(v, Value::Bool(false));
2355    }
2356
2357    #[test]
2358    fn test_clojure_test_is_catch_error() {
2359        // (is expr) catches runtime errors and returns false.
2360        let (_, mut env) = make_env();
2361        eval_src("(require '[clojure.test :refer [is]])", &mut env).unwrap();
2362        let v = eval_src("(is (/ 1 0))", &mut env).unwrap();
2363        assert_eq!(v, Value::Bool(false));
2364    }
2365
2366    #[test]
2367    fn test_clojure_test_deftest_and_run() {
2368        // deftest + run-tests smoke test: counters reflect pass/fail.
2369        let (_, mut env) = make_env();
2370        eval_src(
2371            "(require '[clojure.test :refer [deftest is run-tests]])",
2372            &mut env,
2373        )
2374        .unwrap();
2375        eval_src("(deftest my-passing-test (is (= 1 1)))", &mut env).unwrap();
2376        eval_src("(deftest my-failing-test (is (= 1 2)))", &mut env).unwrap();
2377        let counters = eval_src("(run-tests)", &mut env).unwrap();
2378        // Should have run 2 tests, 1 pass, 1 fail.
2379        if let Value::Map(m) = counters {
2380            let get = |k: &str| {
2381                m.get(&Value::keyword(cljrs_value::Keyword {
2382                    namespace: None,
2383                    name: Arc::from(k),
2384                }))
2385            };
2386            assert_eq!(get("test"), Some(Value::Long(2)));
2387            assert_eq!(get("pass"), Some(Value::Long(1)));
2388            assert_eq!(get("fail"), Some(Value::Long(1)));
2389            assert_eq!(get("error"), Some(Value::Long(0)));
2390        } else {
2391            panic!("expected map from run-tests, got {counters:?}");
2392        }
2393    }
2394
2395    #[test]
2396    fn test_alter_meta_bang() {
2397        // alter-meta! applies fn to var's meta and stores result.
2398        let (_, mut env) = make_env();
2399        eval_src("(def myvar 42)", &mut env).unwrap();
2400        eval_src("(alter-meta! #'myvar assoc :foo :bar)", &mut env).unwrap();
2401        let m = eval_src("(meta #'myvar)", &mut env).unwrap();
2402        if let Value::Map(map) = m {
2403            let foo_key = Value::keyword(cljrs_value::Keyword {
2404                namespace: None,
2405                name: Arc::from("foo"),
2406            });
2407            assert!(map.get(&foo_key).is_some());
2408        } else {
2409            panic!("expected map, got {m:?}");
2410        }
2411    }
2412
2413    #[test]
2414    fn test_catch_runtime_error() {
2415        // (try (/ 1 0) (catch Exception e "caught")) => "caught"
2416        let (_, mut env) = make_env();
2417        let v = eval_src(r#"(try (/ 1 0) (catch Exception e "caught"))"#, &mut env).unwrap();
2418        assert_eq!(v, Value::string("caught".to_string()));
2419    }
2420
2421    #[test]
2422    fn test_ns_resolve() {
2423        let (_, mut env) = make_env();
2424        eval_src("(def somevar 99)", &mut env).unwrap();
2425        // ns-resolve with current ns returns the var.
2426        let v = eval_src("(ns-resolve *ns* 'somevar)", &mut env).unwrap();
2427        assert!(matches!(v, Value::Var(_)));
2428        // ns-resolve for non-existent symbol returns nil.
2429        let v2 = eval_src("(ns-resolve *ns* 'nonexistent)", &mut env).unwrap();
2430        assert_eq!(v2, Value::Nil);
2431    }
2432
2433    // ── Persistent structure virtualization ──────────────────────────────
2434
2435    #[test]
2436    fn test_assoc_chain_virtualized() {
2437        // Assoc chain where intermediates aren't used — should be virtualized.
2438        let v = eval_str(
2439            "(let [m {}
2440                   a (assoc m :x 1)
2441                   b (assoc a :y 2)
2442                   c (assoc b :z 3)]
2443               c)",
2444        )
2445        .unwrap();
2446        // Result should be {:x 1, :y 2, :z 3}.
2447        assert!(matches!(&v, Value::Map(_)));
2448        if let Value::Map(m) = &v {
2449            assert_eq!(m.count(), 3);
2450            assert_eq!(m.get(&Value::keyword(Keyword::simple("x"))), Some(long(1)));
2451            assert_eq!(m.get(&Value::keyword(Keyword::simple("y"))), Some(long(2)));
2452            assert_eq!(m.get(&Value::keyword(Keyword::simple("z"))), Some(long(3)));
2453        }
2454    }
2455
2456    #[test]
2457    fn test_conj_chain_virtualized() {
2458        // Conj chain on a vector.
2459        let v = eval_str(
2460            "(let [v [1]
2461                   a (conj v 2)
2462                   b (conj a 3)
2463                   c (conj b 4)]
2464               c)",
2465        )
2466        .unwrap();
2467        assert_eq!(v, eval_str("[1 2 3 4]").unwrap());
2468    }
2469
2470    #[test]
2471    fn test_assoc_chain_intermediate_used_no_virtualize() {
2472        // If an intermediate is used in the body, virtualization should not apply,
2473        // but the result should still be correct.
2474        let v = eval_str(
2475            "(let [a (assoc {} :x 1)
2476                   b (assoc a :y 2)]
2477               (list (count a) (count b)))",
2478        )
2479        .unwrap();
2480        // a has 1 entry, b has 2.
2481        if let Value::List(l) = &v {
2482            let items: Vec<_> = l.get().iter().cloned().collect();
2483            assert_eq!(items, vec![long(1), long(2)]);
2484        } else {
2485            panic!("expected list, got {:?}", v);
2486        }
2487    }
2488
2489    #[test]
2490    fn test_assoc_chain_on_existing_map() {
2491        // Chain on an existing non-empty map.
2492        let v = eval_str(
2493            "(let [m {:a 1}
2494                   a (assoc m :b 2)
2495                   b (assoc a :c 3)]
2496               b)",
2497        )
2498        .unwrap();
2499        if let Value::Map(m) = &v {
2500            assert_eq!(m.count(), 3);
2501        } else {
2502            panic!("expected map");
2503        }
2504    }
2505
2506    // ── :pre/:post conditions ─────────────────────────────────────────────
2507
2508    #[test]
2509    fn test_post_condition_percent_bound() {
2510        // % must resolve to the return value inside :post conditions.
2511        let v = eval_str("(defn g [x] {:post [(pos? %)]} (inc x)) (g 5)").unwrap();
2512        assert_eq!(v, long(6));
2513    }
2514
2515    #[test]
2516    fn test_post_condition_violation_throws() {
2517        // A failing :post condition must throw.
2518        let r = eval_str("(defn g [x] {:post [(neg? %)]} (inc x)) (g 5)");
2519        assert!(r.is_err(), "expected error from failing :post condition");
2520    }
2521
2522    #[test]
2523    fn test_pre_condition_passes() {
2524        let v = eval_str("(defn g [x] {:pre [(pos? x)]} (inc x)) (g 5)").unwrap();
2525        assert_eq!(v, long(6));
2526    }
2527
2528    #[test]
2529    fn test_pre_condition_violation_throws() {
2530        let r = eval_str("(defn g [x] {:pre [(pos? x)]} (inc x)) (g -1)");
2531        assert!(r.is_err(), "expected error from failing :pre condition");
2532    }
2533
2534    #[test]
2535    fn test_pre_and_post_conditions() {
2536        let v = eval_str("(defn g [x] {:pre [(pos? x)] :post [(> % x)]} (inc x)) (g 3)").unwrap();
2537        assert_eq!(v, long(4));
2538    }
2539
2540    #[test]
2541    fn test_post_condition_no_pre() {
2542        // :post only (no :pre).
2543        let v = eval_str("(defn h [x] {:post [(number? %)]} (inc x)) (h 2)").unwrap();
2544        assert_eq!(v, long(3));
2545    }
2546
2547    #[test]
2548    fn test_pre_condition_no_post() {
2549        // :pre only (no :post); existing test variant without conditions map.
2550        let v = eval_str("(defn h [x] {:pre [(number? x)]} x) (h 42)").unwrap();
2551        assert_eq!(v, long(42));
2552    }
2553}