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