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