Skip to main content

cljrs_runtime/interp/
eval.rs

1//! Top-level `eval` dispatcher and form-to-value conversion.
2
3use std::sync::Arc;
4
5use crate::builtins::form::{expand_pairs, expand_reader_conds, expand_reader_conds_cow};
6use crate::builtins::special::SPECIAL_FORMS;
7use crate::env::env::{Env, GlobalEnv};
8use crate::env::error::{EvalError, EvalResult};
9use crate::interp::apply::eval_call;
10use crate::interp::special::eval_special;
11use crate::interp::syntax_quote::syntax_quote;
12use cljrs_gc::GcPtr;
13use cljrs_reader::Form;
14use cljrs_reader::form::FormKind;
15use cljrs_value::regex::Pattern;
16use cljrs_value::value::SetValue;
17use cljrs_value::{
18    CljxFuture, FutureState, Keyword, MapValue, PersistentHashSet, PersistentList,
19    PersistentVector, Symbol, Value,
20};
21
22/// Evaluate a `Form` in the given `Env`.
23pub fn eval(form: &Form, env: &mut Env) -> EvalResult {
24    if !crate::env::gas::charge(1) {
25        return Err(EvalError::GasExhausted);
26    }
27    match &form.kind {
28        // ── Atoms ─────────────────────────────────────────────────────────
29        FormKind::Nil => Ok(Value::Nil),
30        FormKind::Bool(b) => Ok(Value::Bool(*b)),
31        FormKind::Int(n) => Ok(Value::Long(*n)),
32        FormKind::Float(f) => Ok(Value::Double(*f)),
33        FormKind::Symbolic(f) => Ok(Value::Double(*f)), // ##Inf etc.
34        FormKind::Str(s) => Ok(Value::string(s.clone())),
35        FormKind::Char(c) => Ok(Value::Char(*c)),
36        FormKind::BigInt(s) => crate::builtins::parse_bigint(s),
37        FormKind::BigDecimal(s) => crate::builtins::parse_bigdecimal(s),
38        FormKind::Ratio(s) => crate::builtins::parse_ratio(s),
39        FormKind::Regex(s) => {
40            let r = Pattern::new(s);
41            match r {
42                Ok(r) => Ok(Value::Pattern(GcPtr::new(r))),
43                Err(e) => Err(EvalError::Runtime(e.to_string())),
44            }
45        }
46
47        // ── Identifiers ───────────────────────────────────────────────────
48        FormKind::Symbol(s) => eval_symbol(s, env),
49        FormKind::Keyword(s) => Ok(Value::keyword(Keyword::parse(s))),
50        FormKind::AutoKeyword(s) => {
51            let full = env
52                .globals
53                .resolve_auto_keyword(&env.current_ns, s)
54                .map_err(EvalError::Runtime)?;
55            Ok(Value::keyword(Keyword::parse(&full)))
56        }
57        // A symbol key from an auto-resolved namespaced map: in evaluated
58        // position it names a var, exactly as a written-out symbol key does.
59        FormKind::AutoSymbol(s) => {
60            let full = env
61                .globals
62                .resolve_auto_keyword(&env.current_ns, s)
63                .map_err(EvalError::Runtime)?;
64            eval_symbol(&full, env)
65        }
66
67        // ── Collections ───────────────────────────────────────────────────
68        FormKind::List(forms) => eval_list(forms, env),
69        FormKind::Vector(forms) => {
70            let forms = expand_reader_conds_cow(forms);
71            let mut vals: Vec<Value> = Vec::with_capacity(forms.len());
72            for f in forms.iter() {
73                let _root = crate::env::gc_roots::root_values(&vals);
74                vals.push(eval(f, env)?);
75            }
76            Ok(Value::Vector(GcPtr::new(PersistentVector::from_iter(vals))))
77        }
78        FormKind::Map(forms) => {
79            let forms = expand_pairs(forms).map_err(|_| {
80                EvalError::Runtime("map literal must have an even number of forms".into())
81            })?;
82            let mut pairs: Vec<Value> = Vec::with_capacity(forms.len());
83            for f in forms.iter() {
84                let _root = crate::env::gc_roots::root_values(&pairs);
85                pairs.push(eval(f, env)?);
86            }
87            let kv_pairs: Vec<(Value, Value)> = pairs
88                .chunks(2)
89                .map(|pair| (pair[0].clone(), pair[1].clone()))
90                .collect();
91            Ok(Value::Map(MapValue::from_pairs(kv_pairs)))
92        }
93        FormKind::Set(forms) => {
94            let forms = expand_reader_conds_cow(forms);
95            let mut vals: Vec<Value> = Vec::with_capacity(forms.len());
96            for f in forms.iter() {
97                let _root = crate::env::gc_roots::root_values(&vals);
98                vals.push(eval(f, env)?);
99            }
100            Ok(Value::Set(SetValue::Hash(GcPtr::new(
101                PersistentHashSet::from_iter(vals),
102            ))))
103        }
104
105        // ── Reader macros ─────────────────────────────────────────────────
106        // `'x` sugar: like the `quote` special form, `::kw` and an
107        // auto-resolved map's symbol keys resolve against the reading
108        // namespace before the form becomes data.
109        FormKind::Quote(inner) => {
110            let resolved = crate::builtins::form::resolve_auto_forms(inner, env)?;
111            crate::builtins::form::form_to_value(&resolved)
112        }
113        FormKind::SyntaxQuote(inner) => syntax_quote(inner, env),
114        FormKind::Unquote(_) => Err(EvalError::Runtime("unquote outside syntax-quote".into())),
115        FormKind::UnquoteSplice(_) => Err(EvalError::Runtime(
116            "unquote-splice outside syntax-quote".into(),
117        )),
118        FormKind::Deref(inner) => {
119            let v = eval(inner, env)?;
120            if env.is_async && matches!(v, Value::Future(_)) {
121                return Err(EvalError::Runtime(
122                    "deref (@) on a future is not allowed inside an ^:async function; use (await ...) instead".into(),
123                ));
124            }
125            deref_value(v)
126        }
127        FormKind::Var(inner) => {
128            if let FormKind::Symbol(s) = &inner.kind {
129                let parsed = Symbol::parse(s);
130                let ns: Arc<str> = env.resolve_ns_or_current(parsed.namespace.as_deref());
131                env.globals
132                    .lookup_var_in_ns(&ns, &parsed.name)
133                    .map(Value::Var)
134                    .ok_or_else(|| EvalError::UnboundSymbol(s.clone()))
135            } else {
136                Err(EvalError::Runtime("var requires a symbol".into()))
137            }
138        }
139        FormKind::Meta(meta, form) => {
140            // `^m expr` evaluates `expr`; the annotation becomes runtime
141            // metadata only on a form that constructs an `IObj` (a collection
142            // literal or an `fn`). Everywhere else it is a compile-time hint,
143            // and — as on the JVM — is not evaluated at all.
144            //
145            // `lower::anf` applies the same rule, so a promoted or AOT-compiled
146            // body answers `meta` the same way an interpreted one does.
147            let value = eval(form, env)?;
148            if !form.takes_runtime_meta() {
149                return Ok(value);
150            }
151            let m = crate::builtins::form::expand_meta_annotation(meta, &mut |f| eval(f, env))?;
152            Ok(crate::builtins::form::attach_meta(value, m))
153        }
154
155        // ── Dispatch ──────────────────────────────────────────────────────
156        FormKind::AnonFn(body) => {
157            let expanded = crate::builtins::form::expand_anon_fn(body, form.span.clone());
158            eval(&expanded, env)
159        }
160        FormKind::ReaderCond {
161            splicing: _,
162            clauses,
163        } => eval_reader_cond(clauses, env),
164        FormKind::TaggedLiteral(tag, inner) => eval_tagged_literal(tag, inner, env),
165    }
166}
167
168/// Evaluate a form with a cooperative execution-credit budget.
169///
170/// Nested tree-walker, IR-interpreter, and JIT work shares this budget.  The
171/// existing [`eval`] entry point remains unmetered unless called inside this
172/// dynamic scope.
173pub fn eval_with_gas(form: &Form, env: &mut Env, credits: u64) -> EvalResult {
174    let meter = crate::env::gas::GasMeter::new(credits);
175    let _guard = crate::env::gas::GasGuard::install(meter);
176    eval(form, env)
177}
178
179// ── List / call dispatch ──────────────────────────────────────────────────────
180
181fn eval_list(forms: &[Form], env: &mut Env) -> EvalResult {
182    if forms.is_empty() {
183        return Ok(Value::List(GcPtr::new(PersistentList::empty())));
184    }
185
186    // Expand reader conditionals (both splicing and non-splicing) before dispatch.
187    let expanded: Vec<Form>;
188    let forms: &[Form] = if forms
189        .iter()
190        .any(|f| matches!(f.kind, FormKind::ReaderCond { .. }))
191    {
192        expanded = expand_reader_conds(forms);
193        if expanded.is_empty() {
194            return Ok(Value::List(GcPtr::new(PersistentList::empty())));
195        }
196        &expanded
197    } else {
198        forms
199    };
200
201    // Check for special form.
202    if let Some(s) = forms[0].as_symbol()
203        && is_special_form(s)
204    {
205        return eval_special(s, &forms[1..], env);
206    }
207
208    eval_call(&forms[0], &forms[1..], env)
209}
210
211// ── Symbol resolution ─────────────────────────────────────────────────────────
212
213fn eval_symbol(s: &str, env: &mut Env) -> EvalResult {
214    let sym = Symbol::parse(s);
215
216    // Explicit version suffix (`name@hash` or `ns/name@hash`): always a
217    // namespace-level lookup — no local-frame fallback.
218    #[cfg(not(target_arch = "wasm32"))]
219    if let Some(ref commit) = sym.version.clone() {
220        crate::env::policy::check_versioned_lookup()?;
221        return crate::interp::versioned::resolve_versioned_symbol(&sym, commit, env);
222    }
223    #[cfg(target_arch = "wasm32")]
224    if sym.version.is_some() {
225        return Err(crate::env::error::EvalError::Runtime(
226            "versioned symbols are not supported in WASM".to_string(),
227        ));
228    }
229
230    // Local frames (params, let-bindings, closed-over vars) take priority for
231    // unversioned symbols.
232    if let Some(v) = env.lookup_local_frames(s) {
233        return Ok(v);
234    }
235
236    // Inherited versioned context: if we are evaluating inside a versioned
237    // function body, unversioned same-namespace symbols resolve at the inherited
238    // commit rather than HEAD.  "Same namespace" includes a qualified
239    // self-reference written with the base name (`mylib/x` inside `mylib@hash`).
240    #[cfg(not(target_arch = "wasm32"))]
241    if let Some(commit) = env.versioned_eval_commit.clone() {
242        let is_same_ns = sym.namespace.is_none()
243            || sym.namespace.as_deref() == Some(env.current_ns.as_ref())
244            || sym.namespace.as_deref()
245                == Some(crate::env::versioned::base_ns_name(&env.current_ns));
246        if is_same_ns {
247            return crate::interp::versioned::resolve_versioned_symbol(&sym, &commit, env);
248        }
249    }
250
251    // Fall through to normal global namespace lookup.
252    if let Some(v) = env.globals.lookup_in_ns(&env.current_ns, s) {
253        return Ok(v);
254    }
255
256    // Namespace-qualified external symbol: `ns/name`
257    if s.contains('/')
258        && !s.starts_with('/')
259        && let Some(ns_part) = &sym.namespace
260    {
261        let resolved: Arc<str> = env.resolve_ns_part(ns_part);
262        // Qualified self-reference inside a versioned namespace: `mylib/x`
263        // written in `mylib@hash`'s own source resolves at the pinned commit,
264        // i.e. inside the versioned namespace itself.
265        #[cfg(not(target_arch = "wasm32"))]
266        let resolved: Arc<str> = if env.current_ns.as_ref() != resolved.as_ref()
267            && crate::env::versioned::base_ns_name(&env.current_ns) == resolved.as_ref()
268        {
269            env.current_ns.clone()
270        } else {
271            resolved
272        };
273        if resolved.as_ref() != env.current_ns.as_ref()
274            && let Some(var) = env.globals.lookup_var(&resolved, &sym.name)
275            && GlobalEnv::var_is_private(var.get())
276        {
277            return Err(EvalError::Runtime(format!(
278                "var: {}/{} is not public",
279                resolved, sym.name
280            )));
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::Thrown(CljxFuture::cancelled_error()));
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    // These properties build ONE env per case and evaluate every spelling in
911    // it. `eval_str` bootstraps a whole runtime per call, and a property that
912    // evaluates a dozen spellings across 48 cases pays that hundreds of times.
913    // Nothing here defines anything, so one env per case is equivalent.
914    proptest::proptest! {
915        #![proptest_config(proptest::prelude::ProptestConfig::with_cases(48))]
916        /// Splicing `#?@(:rust mid)` into any container evaluates to the same
917        /// value as inlining `mid` literally - for vector, set, data-list, and
918        /// call arguments. Elements are distinct so set literals stay legal.
919        #[test]
920        fn prop_splice_evals_as_inline_in_every_container(
921            np in 0usize..3, nm in 0usize..3, ns in 0usize..3,
922        ) {
923            let kw_run = |start: usize, n: usize| {
924                (start..start + n)
925                    .map(|i| format!(":v{i}"))
926                    .collect::<Vec<_>>()
927                    .join(" ")
928            };
929            let p = kw_run(0, np);
930            let m = kw_run(np, nm);
931            let s = kw_run(np + nm, ns);
932            let (_g, mut env) = make_env();
933            for (open, close, quote) in
934                [("[", "]", ""), ("#{", "}", ""), ("(", ")", "'"), ("(vector ", ")", "")]
935            {
936                let spliced = format!("{quote}{open}{p} #?@(:rust [{m}]) {s}{close}");
937                let inlined = format!("{quote}{open}{p} {m} {s}{close}");
938                proptest::prop_assert_eq!(
939                    eval_src(&spliced, &mut env).unwrap(),
940                    eval_src(&inlined, &mut env).unwrap(),
941                    "container {}{}",
942                    open,
943                    close
944                );
945            }
946        }
947    }
948
949    // ── Reader conditionals: cross-path and binding-vector properties ─────
950    //
951    // One generated position in a container or binding vector. Names are
952    // assigned by index, so generated elements are distinct and set literals
953    // stay legal.
954
955    #[derive(Clone, Debug)]
956    enum Slot {
957        /// A plain element; contributes 1.
958        Plain,
959        /// `#?(…)`; contributes 1 when the branch key matches, else 0.
960        NonSplice { matches: bool },
961        /// `#?@(…)`; contributes `n` when the branch key matches, else 0.
962        Splice { matches: bool, n: usize },
963    }
964
965    fn slot_strat() -> impl proptest::strategy::Strategy<Value = Slot> {
966        use proptest::strategy::{Just, Strategy};
967        proptest::prop_oneof![
968            Just(Slot::Plain),
969            proptest::bool::ANY.prop_map(|matches| Slot::NonSplice { matches }),
970            (proptest::bool::ANY, 0usize..3).prop_map(|(matches, n)| Slot::Splice { matches, n }),
971        ]
972    }
973
974    /// Render `slots` twice - conditionals written out, and branches inlined -
975    /// with `unit` forms per contributed position. `unit(i)` renders element
976    /// `i`; a unit of two tokens (`x0 0`) keeps pair parity even, which is what
977    /// binding vectors need.
978    fn render_slots(slots: &[Slot], unit: &dyn Fn(usize) -> String) -> (String, String) {
979        let mut next = 0usize;
980        let (mut written, mut inlined) = (Vec::new(), Vec::new());
981        for slot in slots {
982            match *slot {
983                Slot::Plain => {
984                    let u = unit(next);
985                    next += 1;
986                    written.push(u.clone());
987                    inlined.push(u);
988                }
989                Slot::NonSplice { matches } => {
990                    let u = unit(next);
991                    next += 1;
992                    let key = if matches { "rust" } else { "clj" };
993                    written.push(format!("#?(:{key} {u})"));
994                    if matches {
995                        inlined.push(u);
996                    }
997                }
998                Slot::Splice { matches, n } => {
999                    let us: Vec<String> = (0..n)
1000                        .map(|_| {
1001                            let u = unit(next);
1002                            next += 1;
1003                            u
1004                        })
1005                        .collect();
1006                    let key = if matches { "rust" } else { "clj" };
1007                    written.push(format!("#?@(:{key} [{}])", us.join(" ")));
1008                    if matches {
1009                        inlined.extend(us);
1010                    }
1011                }
1012            }
1013        }
1014        (written.join(" "), inlined.join(" "))
1015    }
1016
1017    /// Binding vectors take pairs, and a non-splicing `#?` selects exactly one
1018    /// form - so it cannot carry a `name init` pair. Only plain positions and
1019    /// `#?@` splices can appear there.
1020    fn pair_slot_strat() -> impl proptest::strategy::Strategy<Value = Slot> {
1021        use proptest::strategy::{Just, Strategy};
1022        proptest::prop_oneof![
1023            Just(Slot::Plain),
1024            (proptest::bool::ANY, 0usize..3).prop_map(|(matches, n)| Slot::Splice { matches, n }),
1025        ]
1026    }
1027
1028    fn kw_unit(i: usize) -> String {
1029        format!(":k{i}")
1030    }
1031
1032    /// `x<i> <i>` - one binding pair, so every contributed position keeps the
1033    /// vector's parity even.
1034    fn binding_unit(i: usize) -> String {
1035        format!("x{i} {i}")
1036    }
1037
1038    proptest::proptest! {
1039        #![proptest_config(proptest::prelude::ProptestConfig::with_cases(48))]
1040
1041        /// A form's meaning must not depend on which path reads it. For the same
1042        /// container, evaluating it, quoting it, and syntax-quoting it all agree
1043        /// with the hand-inlined spelling. Before this fix the quoted and
1044        /// syntax-quoted paths dropped or nil-filled splices that the evaluated
1045        /// path expanded.
1046        #[test]
1047        fn prop_splice_agrees_across_eval_quote_and_syntax_quote(
1048            slots in proptest::collection::vec(slot_strat(), 0..5),
1049        ) {
1050            let (written, inlined) = render_slots(&slots, &kw_unit);
1051            let even = inlined.split_whitespace().count().is_multiple_of(2);
1052            let (_g, mut env) = make_env();
1053            for (open, close) in [("[", "]"), ("#{", "}"), ("{", "}")] {
1054                if open == "{" && !even {
1055                    continue;
1056                }
1057                for prefix in ["", "'", "`"] {
1058                    let got = eval_src(&format!("{prefix}{open}{written}{close}"), &mut env);
1059                    let want = eval_src(&format!("{prefix}{open}{inlined}{close}"), &mut env);
1060                    proptest::prop_assert_eq!(
1061                        got.map_err(|e| e.to_string()),
1062                        want.map_err(|e| e.to_string()),
1063                        "prefix {:?} container {}{}", prefix, open, close
1064                    );
1065                }
1066            }
1067            // Data lists have no evaluated spelling; check both quoted forms.
1068            for prefix in ["'", "`"] {
1069                let got = eval_src(&format!("{prefix}({written})"), &mut env);
1070                let want = eval_src(&format!("{prefix}({inlined})"), &mut env);
1071                proptest::prop_assert_eq!(
1072                    got.map_err(|e| e.to_string()),
1073                    want.map_err(|e| e.to_string()),
1074                    "prefix {:?} list", prefix
1075                );
1076            }
1077        }
1078
1079        /// Every binding vector resolves conditionals: `let*`, `loop*` and
1080        /// `binding` bind exactly what the inlined spelling binds.
1081        #[test]
1082        fn prop_splice_in_binding_vectors_equals_inline(
1083            slots in proptest::collection::vec(pair_slot_strat(), 0..4),
1084        ) {
1085            let (written, inlined) = render_slots(&slots, &binding_unit);
1086            let names: Vec<String> = inlined
1087                .split_whitespace()
1088                .step_by(2)
1089                .map(str::to_string)
1090                .collect();
1091            let body = format!("[{}]", names.join(" "));
1092            let (_g, mut env) = make_env();
1093            for head in ["let*", "loop*"] {
1094                let got = eval_src(&format!("({head} [{written}] {body})"), &mut env);
1095                let want = eval_src(&format!("({head} [{inlined}] {body})"), &mut env);
1096                proptest::prop_assert_eq!(
1097                    got.map_err(|e| e.to_string()),
1098                    want.map_err(|e| e.to_string()),
1099                    "{}", head
1100                );
1101            }
1102        }
1103    }
1104
1105    #[test]
1106    fn quoted_map_splice_lands_in_key_value_positions() {
1107        assert_eq!(
1108            eval_str("'{:a 1 #?@(:rust [:b 2]) :c 3}").unwrap(),
1109            eval_str("{:a 1 :b 2 :c 3}").unwrap()
1110        );
1111    }
1112
1113    #[test]
1114    fn syntax_quoted_splice_is_expanded_not_nil() {
1115        assert_eq!(
1116            eval_str("`(1 #?@(:rust [2 3]) 4)").unwrap(),
1117            eval_str("'(1 2 3 4)").unwrap()
1118        );
1119    }
1120
1121    #[test]
1122    fn map_literal_with_only_an_unmatched_conditional_reads_as_empty() {
1123        // The written parity is odd, the expansion is empty - the reader must
1124        // defer rather than reject.
1125        assert_eq!(eval_str("{#?(:clj :a)}").unwrap(), eval_str("{}").unwrap());
1126    }
1127
1128    #[test]
1129    fn loop_binding_vector_expands_splices() {
1130        assert_eq!(eval_str("(loop* [#?@(:rust [x 1])] x)").unwrap(), long(1));
1131    }
1132
1133    // ── Error cases ───────────────────────────────────────────────────────
1134
1135    #[test]
1136    fn test_unbound_symbol() {
1137        let r = eval_str("undefined-var-xyz");
1138        assert!(matches!(r, Err(EvalError::UnboundSymbol(_))));
1139    }
1140
1141    #[test]
1142    fn test_wrong_arity() {
1143        let (_, mut env) = make_env();
1144        eval_src("(defn one-arg [x] x)", &mut env).unwrap();
1145        let r = eval_src("(one-arg 1 2)", &mut env);
1146        assert!(matches!(r, Err(EvalError::Arity { .. })));
1147    }
1148
1149    #[test]
1150    fn test_not_callable() {
1151        let r = eval_str("(42 1 2)");
1152        assert!(matches!(r, Err(EvalError::NotCallable(_))));
1153    }
1154
1155    // ── Higher-order functions (bootstrap) ────────────────────────────────
1156
1157    #[test]
1158    fn test_map_fn() {
1159        assert_eq!(
1160            eval_str("(vec (map inc [1 2 3]))").unwrap(),
1161            eval_str("[2 3 4]").unwrap()
1162        );
1163    }
1164
1165    #[test]
1166    fn test_filter_fn() {
1167        assert_eq!(
1168            eval_str("(vec (filter odd? [1 2 3 4 5]))").unwrap(),
1169            eval_str("[1 3 5]").unwrap()
1170        );
1171    }
1172
1173    #[test]
1174    fn test_reduce_fn() {
1175        assert_eq!(eval_str("(reduce + [1 2 3 4 5])").unwrap(), long(15));
1176    }
1177
1178    #[test]
1179    fn test_apply_fn() {
1180        assert_eq!(eval_str("(apply + [1 2 3])").unwrap(), long(6));
1181    }
1182
1183    #[test]
1184    fn test_atom_ops() {
1185        let (_, mut env) = make_env();
1186        eval_src("(def a (atom 0))", &mut env).unwrap();
1187        eval_src("(swap! a inc)", &mut env).unwrap();
1188        assert_eq!(eval_src("(deref a)", &mut env).unwrap(), long(1));
1189    }
1190
1191    #[test]
1192    fn test_when_macro() {
1193        assert_eq!(eval_str("(when true 42)").unwrap(), long(42));
1194        assert_eq!(eval_str("(when false 42)").unwrap(), Value::Nil);
1195    }
1196
1197    #[test]
1198    fn test_cond_macro() {
1199        assert_eq!(eval_str("(cond false 1 true 2)").unwrap(), long(2));
1200    }
1201
1202    #[test]
1203    fn test_and_or() {
1204        assert_eq!(eval_str("(and 1 2 3)").unwrap(), long(3));
1205        assert_eq!(eval_str("(and 1 false 3)").unwrap(), bool_v(false));
1206        assert_eq!(eval_str("(or false nil 42)").unwrap(), long(42));
1207        assert_eq!(eval_str("(or false nil)").unwrap(), Value::Nil);
1208    }
1209
1210    // ── Phase 5: Lazy sequences ───────────────────────────────────────────
1211
1212    #[test]
1213    fn test_lazy_range() {
1214        assert_eq!(
1215            eval_str("(= (into [] (take 5 (range))) [0 1 2 3 4])").unwrap(),
1216            bool_v(true)
1217        );
1218    }
1219
1220    #[test]
1221    fn test_lazy_range_bounded() {
1222        assert_eq!(
1223            eval_str("(= (into [] (range 3)) [0 1 2])").unwrap(),
1224            bool_v(true)
1225        );
1226    }
1227
1228    #[test]
1229    fn test_lazy_iterate() {
1230        assert_eq!(
1231            eval_str("(= (into [] (take 3 (iterate inc 0))) [0 1 2])").unwrap(),
1232            bool_v(true)
1233        );
1234    }
1235
1236    #[test]
1237    fn test_lazy_repeat() {
1238        assert_eq!(
1239            eval_str("(= (into [] (take 3 (repeat :x))) [:x :x :x])").unwrap(),
1240            bool_v(true)
1241        );
1242    }
1243
1244    #[test]
1245    fn test_lazy_cycle() {
1246        assert_eq!(
1247            eval_str("(= (into [] (take 5 (cycle [1 2]))) [1 2 1 2 1])").unwrap(),
1248            bool_v(true)
1249        );
1250    }
1251
1252    // ── Phase 5: Associative destructuring ───────────────────────────────
1253
1254    #[test]
1255    fn test_assoc_destructure() {
1256        assert_eq!(
1257            eval_str("(let [{:keys [a b]} {:a 1 :b 2}] (+ a b))").unwrap(),
1258            long(3)
1259        );
1260    }
1261
1262    #[test]
1263    fn test_assoc_destructure_or() {
1264        assert_eq!(
1265            eval_str("(let [{:keys [a b] :or {b 99}} {:a 1}] b)").unwrap(),
1266            long(99)
1267        );
1268    }
1269
1270    // ── Phase 5: letfn ───────────────────────────────────────────────────
1271
1272    #[test]
1273    fn test_letfn() {
1274        assert_eq!(
1275            eval_str("(letfn [(fact [n] (if (= n 0) 1 (* n (fact (dec n)))))] (fact 5))").unwrap(),
1276            long(120)
1277        );
1278    }
1279
1280    // letfn scope is MUTUAL, not sequential: every binding is visible to every
1281    // other one regardless of order. These pin the three-pass construction in
1282    // `eval_letfn` — a closure captures values rather than cells, so without the
1283    // pass-3 back-patch a forward reference silently sees the nil placeholder
1284    // pass 1 left behind.
1285
1286    #[test]
1287    fn test_letfn_forward_reference() {
1288        assert_eq!(
1289            eval_str("(letfn [(f [n] (g n)) (g [n] (* n 2))] (f 21))").unwrap(),
1290            long(42)
1291        );
1292    }
1293
1294    #[test]
1295    fn test_letfn_mutual_recursion() {
1296        // `my-even?` names `my-odd?` before it is built — the direction that was
1297        // broken — and `my-odd?` names `my-even?` backwards.
1298        let defs = "(letfn [(my-even? [n] (if (= n 0) true (my-odd? (dec n))))
1299                            (my-odd? [n] (if (= n 0) false (my-even? (dec n))))]";
1300        assert_eq!(
1301            eval_str(&format!("{defs} (my-even? 10))")).unwrap(),
1302            Value::Bool(true)
1303        );
1304        assert_eq!(
1305            eval_str(&format!("{defs} (my-odd? 10))")).unwrap(),
1306            Value::Bool(false)
1307        );
1308    }
1309
1310    #[test]
1311    fn test_letfn_three_way_mutual_recursion() {
1312        let defs = "(letfn [(a [n] (if (= n 0) 1 (b (dec n))))
1313                            (b [n] (if (= n 0) 2 (c (dec n))))
1314                            (c [n] (if (= n 0) 3 (a (dec n))))]";
1315        for (arg, want) in [(0, 1), (1, 2), (2, 3), (3, 1)] {
1316            assert_eq!(
1317                eval_str(&format!("{defs} (a {arg}))")).unwrap(),
1318                long(want),
1319                "(a {arg})"
1320            );
1321        }
1322    }
1323
1324    #[test]
1325    fn test_letfn_closes_over_outer_local() {
1326        assert_eq!(
1327            eval_str("(let [start 10] (letfn [(step [n] (+ n start))] (step 5)))").unwrap(),
1328            long(15)
1329        );
1330    }
1331
1332    #[test]
1333    fn test_letfn_shadows_enclosing_binding() {
1334        // The letfn binding wins over the `let` of the same name, and siblings
1335        // that name it see the fn, not the shadowed value.
1336        assert_eq!(
1337            eval_str("(let [f 1] (letfn [(g [] (f)) (f [] 7)] (g)))").unwrap(),
1338            long(7)
1339        );
1340    }
1341
1342    #[test]
1343    fn test_letfn_binding_name_must_be_a_symbol() {
1344        assert!(eval_str("(letfn [(42 [n] n)] 1)").is_err());
1345    }
1346
1347    // ── Phase 5: namespace ops ────────────────────────────────────────────
1348
1349    #[test]
1350    fn test_in_ns() {
1351        let (_, mut env) = make_env();
1352        eval_src("(in-ns 'mytest)", &mut env).unwrap();
1353        assert_eq!(env.current_ns.as_ref(), "mytest");
1354        eval_src("(in-ns 'user)", &mut env).unwrap();
1355        assert_eq!(env.current_ns.as_ref(), "user");
1356    }
1357
1358    // ── Phase 5: spit / slurp ─────────────────────────────────────────────
1359
1360    #[test]
1361    fn test_spit_slurp() {
1362        let path = std::env::temp_dir().join("cljrs_test_spit_slurp.txt");
1363        let path_str = path.to_str().unwrap();
1364        let src = format!(
1365            r#"(do (spit "{}" "hello clojurust") (slurp "{}"))"#,
1366            path_str, path_str
1367        );
1368        let result = eval_str(&src).unwrap();
1369        if let Value::Str(s) = result {
1370            assert_eq!(s.get().as_str(), "hello clojurust");
1371        } else {
1372            panic!("expected string result from slurp");
1373        }
1374        let _ = std::fs::remove_file(path);
1375    }
1376
1377    // ── Phase 5: update-in ───────────────────────────────────────────────
1378
1379    #[test]
1380    fn test_update_in() {
1381        assert_eq!(
1382            eval_str("(= (update-in {:a {:b 1}} [:a :b] inc) {:a {:b 2}})").unwrap(),
1383            bool_v(true)
1384        );
1385    }
1386
1387    // ── Phase 5: if-let / when-let ────────────────────────────────────────
1388
1389    #[test]
1390    fn test_if_let_truthy() {
1391        assert_eq!(eval_str("(if-let [x 42] x :nope)").unwrap(), long(42));
1392    }
1393
1394    #[test]
1395    fn test_if_let_falsy() {
1396        assert_eq!(
1397            eval_str("(if-let [x nil] x :nope)").unwrap(),
1398            eval_str(":nope").unwrap()
1399        );
1400    }
1401
1402    #[test]
1403    fn test_when_let_truthy() {
1404        assert_eq!(eval_str("(when-let [x 7] (* x 2))").unwrap(), long(14));
1405    }
1406
1407    #[test]
1408    fn test_when_let_falsy() {
1409        assert_eq!(eval_str("(when-let [x nil] 99)").unwrap(), Value::Nil);
1410    }
1411
1412    // ── Phase 5: math functions ───────────────────────────────────────────
1413
1414    #[test]
1415    fn test_math_trig() {
1416        // sin(0) = 0, cos(0) = 1
1417        assert_eq!(eval_str("(Math/sin 0)").unwrap(), Value::Double(0.0));
1418        assert_eq!(eval_str("(Math/cos 0)").unwrap(), Value::Double(1.0));
1419    }
1420
1421    #[test]
1422    fn test_math_constants() {
1423        assert!(
1424            matches!(eval_str("Math/PI").unwrap(), Value::Double(v) if (v - std::f64::consts::PI).abs() < 1e-10)
1425        );
1426        assert!(
1427            matches!(eval_str("Math/E").unwrap(), Value::Double(v) if (v - std::f64::consts::E).abs() < 1e-10)
1428        );
1429    }
1430
1431    #[test]
1432    fn test_math_log_exp() {
1433        // exp(0) = 1, log(1) = 0
1434        assert_eq!(eval_str("(Math/exp 0)").unwrap(), Value::Double(1.0));
1435        assert_eq!(eval_str("(Math/log 1)").unwrap(), Value::Double(0.0));
1436    }
1437
1438    // ── Phase 6: Protocols & Multimethods ─────────────────────────────────
1439
1440    #[test]
1441    fn test_defprotocol() {
1442        // Defining a protocol creates a callable ProtocolFn that errors without impl.
1443        let result = eval_str(
1444            r#"
1445            (defprotocol Greet
1446              (greet [this]))
1447            (greet "hello")
1448            "#,
1449        );
1450        assert!(result.is_err());
1451        let msg = result.unwrap_err().to_string();
1452        assert!(msg.contains("No implementation"), "got: {msg}");
1453    }
1454
1455    #[test]
1456    fn test_extend_type() {
1457        let result = eval_str(
1458            r#"
1459            (defprotocol Greet
1460              (greet [this]))
1461            (extend-type String
1462              Greet
1463              (greet [this] (str "Hello, " this "!")))
1464            (greet "world")
1465            "#,
1466        )
1467        .unwrap();
1468        assert_eq!(result, Value::string("Hello, world!"));
1469    }
1470
1471    #[test]
1472    fn test_protocol_dispatch() {
1473        let result = eval_str(
1474            r#"
1475            (defprotocol Describable
1476              (describe [this]))
1477            (extend-type String
1478              Describable
1479              (describe [this] (str "string:" this)))
1480            (extend-type Long
1481              Describable
1482              (describe [this] (str "long:" this)))
1483            [(describe "hi") (describe 42)]
1484            "#,
1485        )
1486        .unwrap();
1487        assert!(matches!(result, Value::Vector(_)));
1488        let s = format!("{}", result);
1489        assert!(s.contains("string:hi"), "got: {s}");
1490        assert!(s.contains("long:42"), "got: {s}");
1491    }
1492
1493    #[test]
1494    fn test_extend_protocol() {
1495        let result = eval_str(
1496            r#"
1497            (defprotocol Showable
1498              (show [this]))
1499            (extend-protocol Showable
1500              String
1501              (show [this] (str "S:" this))
1502              Long
1503              (show [this] (str "L:" this)))
1504            [(show "x") (show 7)]
1505            "#,
1506        )
1507        .unwrap();
1508        let s = format!("{}", result);
1509        assert!(s.contains("S:x"), "got: {s}");
1510        assert!(s.contains("L:7"), "got: {s}");
1511    }
1512
1513    #[test]
1514    fn test_extend_via_metadata() {
1515        // `:extend-via-metadata true` lets an instance implement a protocol by
1516        // carrying the impl fn in its own metadata, keyed by the protocol
1517        // method's fully-qualified symbol (matching real Clojure's
1518        // `MethodImplCache` dispatch, which looks up `(.sym cache)` in
1519        // `(meta x)`) — no `extend-type`/`extend-protocol` needed. Idiomatic
1520        // usage produces that qualified symbol via syntax-quote.
1521        let result = eval_str(
1522            r#"
1523            (defprotocol IRender
1524              :extend-via-metadata true
1525              (create-element [this tag-name]))
1526            (def renderer (with-meta {} {`create-element (fn [this tag-name] (str "made-" tag-name))}))
1527            (create-element renderer "div")
1528            "#,
1529        )
1530        .unwrap();
1531        assert_eq!(result, Value::string("made-div"));
1532    }
1533
1534    #[test]
1535    fn test_extend_via_metadata_falls_back_to_type_tag() {
1536        // Metadata impls take priority, but a value without metadata still
1537        // dispatches on its type tag as usual.
1538        let result = eval_str(
1539            r#"
1540            (defprotocol IRender
1541              :extend-via-metadata true
1542              (create-element [this tag-name]))
1543            (extend-type Map
1544              IRender
1545              (create-element [this tag-name] (str "type-tag-" tag-name)))
1546            [(create-element {} "span")
1547             (create-element (with-meta {} {`create-element (fn [this tag-name] (str "meta-" tag-name))}) "div")]
1548            "#,
1549        )
1550        .unwrap();
1551        let s = format!("{}", result);
1552        assert!(s.contains("type-tag-span"), "got: {s}");
1553        assert!(s.contains("meta-div"), "got: {s}");
1554    }
1555
1556    #[test]
1557    fn test_extend_via_metadata_cross_ns() {
1558        // Mirrors Replicant's mutation_log fake renderer: `IRender` is
1559        // defined in `replicant.core`, and a test namespace `:refer`s the
1560        // method and implements it purely via metadata (no `extend-type`).
1561        // Syntax-quoting `create-element` there must resolve to the
1562        // protocol's home namespace (`replicant.core/create-element`), which
1563        // is exactly the key the dispatcher looks up.
1564        let dir = temp_ns_dir("extend_via_metadata_cross_ns");
1565        std::fs::create_dir_all(dir.join("replicant")).unwrap();
1566        std::fs::write(
1567            dir.join("replicant").join("core.cljrs"),
1568            r#"(ns replicant.core)
1569               (defprotocol IRender
1570                 :extend-via-metadata true
1571                 (create-element [this tag-name]))"#,
1572        )
1573        .unwrap();
1574        let (_, mut env) = make_env_with_paths(vec![dir]);
1575        let result = eval_src(
1576            r#"
1577            (ns mutation-log-test
1578              (:require [replicant.core :refer [create-element]]))
1579            (def renderer (with-meta {} {`create-element (fn [this tag-name] (str "made-" tag-name))}))
1580            (create-element renderer "div")
1581            "#,
1582            &mut env,
1583        )
1584        .unwrap();
1585        assert_eq!(result, Value::string("made-div"));
1586    }
1587
1588    #[test]
1589    fn test_extend_via_metadata_cross_ns_via_alias() {
1590        // The `:refer` case above never touches the buggy path: a `:refer`d
1591        // bare symbol resolves through `lookup_var_in_ns`, which was always
1592        // correct. Real usage syntax-quotes an *aliased* symbol instead —
1593        // `` `p/attached? `` — which used to hit `qualify_symbol`'s "already
1594        // has a slash, keep as-is" branch and leak the alias text (`p/...`)
1595        // into the produced symbol instead of resolving it to the protocol's
1596        // home namespace (`replicant.protocols/...`), so the metadata key
1597        // never matched.
1598        let dir = temp_ns_dir("extend_via_metadata_cross_ns_via_alias");
1599        std::fs::create_dir_all(dir.join("replicant")).unwrap();
1600        std::fs::write(
1601            dir.join("replicant").join("protocols.cljrs"),
1602            r#"(ns replicant.protocols)
1603               (defprotocol IRender
1604                 :extend-via-metadata true
1605                 (attached? [this el]))"#,
1606        )
1607        .unwrap();
1608        let (_, mut env) = make_env_with_paths(vec![dir]);
1609        let result = eval_src(
1610            r#"
1611            (ns mutation-log-test
1612              (:require [replicant.protocols :as p]))
1613            (def r (with-meta {:log []} {`p/attached? (fn [_ el] el)}))
1614            (p/attached? r :el)
1615            "#,
1616            &mut env,
1617        )
1618        .unwrap();
1619        assert_eq!(result, Value::keyword(Keyword::simple("el")));
1620    }
1621
1622    #[test]
1623    fn test_satisfies() {
1624        let result = eval_str(
1625            r#"
1626            (defprotocol Animal
1627              (speak [this]))
1628            (extend-type String
1629              Animal
1630              (speak [this] this))
1631            [(satisfies? Animal "dog") (satisfies? Animal 42)]
1632            "#,
1633        )
1634        .unwrap();
1635        let s = format!("{}", result);
1636        assert!(s.contains("true"), "got: {s}");
1637        assert!(s.contains("false"), "got: {s}");
1638    }
1639
1640    #[test]
1641    fn test_defmulti_defmethod() {
1642        // Note: fn param destructuring not yet supported; use explicit map lookups.
1643        let result = eval_str(
1644            r#"
1645            (defmulti area :shape)
1646            (defmethod area :circle [m] (* 3 (:r m) (:r m)))
1647            (defmethod area :rectangle [m] (* (:w m) (:h m)))
1648            [(area {:shape :circle :r 2}) (area {:shape :rectangle :w 3 :h 4})]
1649            "#,
1650        )
1651        .unwrap();
1652        let s = format!("{}", result);
1653        // circle: 3*2*2=12, rectangle: 3*4=12
1654        assert!(s.contains("12"), "got: {s}");
1655    }
1656
1657    #[test]
1658    fn test_default_dispatch() {
1659        let result = eval_str(
1660            r#"
1661            (defmulti classify :kind)
1662            (defmethod classify :default [x] :unknown)
1663            (defmethod classify :cat [x] :meow)
1664            [(classify {:kind :dog}) (classify {:kind :cat})]
1665            "#,
1666        )
1667        .unwrap();
1668        let s = format!("{}", result);
1669        assert!(s.contains(":unknown"), "got: {s}");
1670        assert!(s.contains(":meow"), "got: {s}");
1671    }
1672
1673    #[test]
1674    fn test_prefer_method() {
1675        // prefer-method shouldn't error; just records preference
1676        let result = eval_str(
1677            r#"
1678            (defmulti foo identity)
1679            (defmethod foo :a [x] 1)
1680            (prefer-method foo :a :b)
1681            (foo :a)
1682            "#,
1683        )
1684        .unwrap();
1685        assert_eq!(result, Value::Long(1));
1686    }
1687
1688    #[test]
1689    fn test_remove_method() {
1690        let result = eval_str(
1691            r#"
1692            (defmulti bar identity)
1693            (defmethod bar :x [_] 99)
1694            (remove-method bar :x)
1695            (bar :x)
1696            "#,
1697        );
1698        assert!(result.is_err());
1699        let msg = result.unwrap_err().to_string();
1700        assert!(msg.contains("No method"), "got: {msg}");
1701    }
1702
1703    // ── Phase 7: Concurrency primitives ──────────────────────────────────────
1704
1705    #[test]
1706    fn test_compare_and_set() {
1707        let result = eval_str(
1708            r#"
1709            (let [a (atom 10)]
1710              [(compare-and-set! a 10 20)   ; succeeds: 10 == 10
1711               (compare-and-set! a 10 30)   ; fails:    20 != 10
1712               @a])
1713            "#,
1714        )
1715        .unwrap();
1716        let s = format!("{}", result);
1717        assert!(s.contains("true"), "got: {s}");
1718        assert!(s.contains("false"), "got: {s}");
1719        assert!(s.contains("20"), "got: {s}");
1720    }
1721
1722    #[test]
1723    fn test_volatile() {
1724        let result = eval_str(
1725            r#"
1726            (let [v (volatile! 1)]
1727              (vreset! v 2)
1728              (vswap! v + 10)
1729              @v)
1730            "#,
1731        )
1732        .unwrap();
1733        assert_eq!(result, Value::Long(12));
1734    }
1735
1736    #[test]
1737    fn test_delay() {
1738        // Body should not be evaluated until forced.
1739        let result = eval_str(
1740            r#"
1741            (let [calls (atom 0)
1742                  d (delay (swap! calls inc) 42)]
1743              [@calls (force d) @calls (force d) @calls])
1744            "#,
1745        )
1746        .unwrap();
1747        let s = format!("{}", result);
1748        // calls starts at 0, force evaluates body once (returns 42), second force uses cache
1749        // s = [0 42 1 42 1]
1750        assert!(s.starts_with("[0 42 1 42 1]"), "got: {s}");
1751    }
1752
1753    #[test]
1754    fn test_realized() {
1755        let result = eval_str(
1756            r#"
1757            (let [d (delay 99)]
1758              [(realized? d) (force d) (realized? d)])
1759            "#,
1760        )
1761        .unwrap();
1762        let s = format!("{}", result);
1763        assert!(s.starts_with("[false 99 true]"), "got: {s}");
1764    }
1765
1766    #[test]
1767    fn test_promise() {
1768        let result = eval_str(
1769            r#"
1770            (let [p (promise)]
1771              (deliver p 42)
1772              (deliver p 99)  ; second deliver is ignored
1773              @p)
1774            "#,
1775        )
1776        .unwrap();
1777        assert_eq!(result, Value::Long(42));
1778    }
1779
1780    #[test]
1781    #[ignore = "clojure.core/future is undefined by design: @f deadlocks the one \
1782                thread the task needs (GcPtr: !Send). cljrs.core.experimental/future \
1783                is the cooperative stand-in, read with (await f)"]
1784    fn test_future() {
1785        let result = eval_str(
1786            r#"
1787            (let [f (future (+ 1 2))]
1788              @f)
1789            "#,
1790        )
1791        .unwrap();
1792        assert_eq!(result, Value::Long(3));
1793    }
1794
1795    #[test]
1796    #[ignore = "agent not yet implemented (Phase A1 — GcPtr: !Send)"]
1797    fn test_agent_send() {
1798        let result = eval_str(
1799            r#"
1800            (let [a (agent 0)]
1801              (send a + 1)
1802              (send a + 2)
1803              (await-agent a)
1804              @a)
1805            "#,
1806        )
1807        .unwrap();
1808        assert_eq!(result, Value::Long(3));
1809    }
1810
1811    #[test]
1812    #[ignore = "agent not yet implemented (Phase A1 — GcPtr: !Send)"]
1813    fn test_agent_error_restart() {
1814        let result = eval_str(
1815            r#"
1816            (let [a (agent 10)]
1817              (send a (fn [_] (throw (ex-info "boom" {}))))
1818              (await-agent a)
1819              (let [err (agent-error a)]
1820                (restart-agent a 99)
1821                [err @a]))
1822            "#,
1823        )
1824        .unwrap();
1825        let s = format!("{}", result);
1826        // err should be a string containing "boom", @a should be 99
1827        assert!(s.contains("boom"), "got: {s}");
1828        assert!(s.contains("99"), "got: {s}");
1829    }
1830
1831    #[test]
1832    fn test_defrecord_basic() {
1833        // Constructor and field access via keyword.
1834        let result = eval_str(
1835            r#"
1836            (defrecord Point [x y])
1837            (let [p (->Point 3 4)]
1838              [(:x p) (:y p)])
1839            "#,
1840        )
1841        .unwrap();
1842        assert_eq!(result.to_string(), "[3 4]");
1843    }
1844
1845    #[test]
1846    fn test_defrecord_map_constructor() {
1847        let result = eval_str(
1848            r#"
1849            (defrecord Color [r g b])
1850            (let [c (map->Color {:r 255 :g 128 :b 0})]
1851              [(:r c) (:g c) (:b c)])
1852            "#,
1853        )
1854        .unwrap();
1855        assert_eq!(result.to_string(), "[255 128 0]");
1856    }
1857
1858    #[test]
1859    fn test_defrecord_assoc() {
1860        // assoc on a record returns a new record of the same type.
1861        let result = eval_str(
1862            r#"
1863            (defrecord Pt [x y])
1864            (let [p (->Pt 1 2)
1865                  q (assoc p :x 99)]
1866              [(:x q) (:y q) (record? q)])
1867            "#,
1868        )
1869        .unwrap();
1870        assert_eq!(result.to_string(), "[99 2 true]");
1871    }
1872
1873    #[test]
1874    fn test_defrecord_with_protocol() {
1875        let result = eval_str(
1876            r#"
1877            (defprotocol IShape
1878              (area [this]))
1879            (defrecord Circle [radius]
1880              IShape
1881              (area [this] (* 3 (:radius this) (:radius this))))
1882            (let [c (->Circle 5)]
1883              (area c))
1884            "#,
1885        )
1886        .unwrap();
1887        assert_eq!(result, cljrs_value::Value::Long(75));
1888    }
1889
1890    #[test]
1891    fn test_instance_q() {
1892        let result = eval_str(
1893            r#"
1894            (defrecord Dog [name])
1895            (let [d (->Dog "Rex")]
1896              [(instance? Dog d) (instance? Dog 42)])
1897            "#,
1898        )
1899        .unwrap();
1900        assert_eq!(result.to_string(), "[true false]");
1901    }
1902
1903    #[test]
1904    fn test_reify_basic() {
1905        let result = eval_str(
1906            r#"
1907            (defprotocol IGreet
1908              (greet [this name]))
1909            (let [greeter (reify IGreet
1910                            (greet [this name] (str "Hello, " name "!")))]
1911              (greet greeter "World"))
1912            "#,
1913        )
1914        .unwrap();
1915        assert_eq!(result.to_string(), "\"Hello, World!\"");
1916    }
1917
1918    // ── require / load-file ───────────────────────────────────────────────
1919
1920    fn temp_ns_dir(test_name: &str) -> std::path::PathBuf {
1921        let dir = std::env::temp_dir().join(format!("cljrs_test_{test_name}"));
1922        let _ = std::fs::remove_dir_all(&dir);
1923        std::fs::create_dir_all(&dir).unwrap();
1924        dir
1925    }
1926
1927    fn make_env_with_paths(paths: Vec<std::path::PathBuf>) -> (Arc<GlobalEnv>, Env) {
1928        let globals = crate::Runtime::builder()
1929            .execution_mode(crate::ExecutionMode::TreeWalk)
1930            .eager_clojure_test(true)
1931            .source_paths(paths)
1932            .build()
1933            .expect("runtime")
1934            .into_globals();
1935        let env = Env::new(globals.clone(), "user");
1936        (globals, env)
1937    }
1938
1939    #[test]
1940    fn test_require_as() {
1941        let dir = temp_ns_dir("require_as");
1942        std::fs::write(
1943            dir.join("mylib.cljrs"),
1944            "(ns mylib) (defn greet [n] (str \"hello \" n))",
1945        )
1946        .unwrap();
1947        let (_, mut env) = make_env_with_paths(vec![dir]);
1948        let result = eval_src("(require '[mylib :as ml]) (ml/greet \"world\")", &mut env).unwrap();
1949        assert_eq!(result.to_string(), "\"hello world\"");
1950    }
1951
1952    #[test]
1953    fn test_require_refer() {
1954        let dir = temp_ns_dir("require_refer");
1955        std::fs::write(
1956            dir.join("myutil.cljrs"),
1957            "(ns myutil) (defn twice [x] (* 2 x))",
1958        )
1959        .unwrap();
1960        let (_, mut env) = make_env_with_paths(vec![dir]);
1961        let result = eval_src("(require '[myutil :refer [twice]]) (twice 21)", &mut env).unwrap();
1962        assert_eq!(result, Value::Long(42));
1963    }
1964
1965    #[test]
1966    fn test_require_refer_all() {
1967        let dir = temp_ns_dir("require_refer_all");
1968        std::fs::write(
1969            dir.join("mymath.cljrs"),
1970            "(ns mymath) (defn square [x] (* x x))",
1971        )
1972        .unwrap();
1973        let (_, mut env) = make_env_with_paths(vec![dir]);
1974        let result = eval_src("(require '[mymath :refer :all]) (square 7)", &mut env).unwrap();
1975        assert_eq!(result, Value::Long(49));
1976    }
1977
1978    #[test]
1979    fn test_ns_require_clause() {
1980        let dir = temp_ns_dir("ns_require");
1981        std::fs::write(
1982            dir.join("greeter.cljrs"),
1983            "(ns greeter) (defn hi [n] (str \"Hi \" n))",
1984        )
1985        .unwrap();
1986        let (_, mut env) = make_env_with_paths(vec![dir]);
1987        let result = eval_src(
1988            "(ns myapp (:require [greeter :as g])) (g/hi \"Alice\")",
1989            &mut env,
1990        )
1991        .unwrap();
1992        assert_eq!(result.to_string(), "\"Hi Alice\"");
1993    }
1994
1995    #[test]
1996    fn test_var_quote_alias_resolution() {
1997        // #'alias/sym must resolve the alias to the full namespace, just like
1998        // a regular function call does (issue #187).
1999        let dir = temp_ns_dir("var_quote_alias");
2000        // lib.core maps to lib/core.cljrs on the source path.
2001        std::fs::create_dir_all(dir.join("lib")).unwrap();
2002        std::fs::write(
2003            dir.join("lib/core.cljrs"),
2004            "(ns lib.core) (defn public [x] (* x 2))",
2005        )
2006        .unwrap();
2007        let (_, mut env) = make_env_with_paths(vec![dir]);
2008        // Regular call via alias must work first.
2009        let call_result = eval_src("(require '[lib.core :as l]) (l/public 21)", &mut env).unwrap();
2010        assert_eq!(call_result, Value::Long(42));
2011        // #'alias/sym reader form.
2012        let var_result = eval_src("#'l/public", &mut env).unwrap();
2013        assert!(
2014            matches!(var_result, Value::Var(_)),
2015            "expected Var, got {var_result:?}"
2016        );
2017        assert_eq!(var_result.to_string(), "#'lib.core/public");
2018        // (var alias/sym) special form must also resolve the alias.
2019        let var_special = eval_src("(var l/public)", &mut env).unwrap();
2020        assert_eq!(var_special.to_string(), "#'lib.core/public");
2021    }
2022
2023    #[test]
2024    fn test_require_idempotent() {
2025        let dir = temp_ns_dir("require_idempotent");
2026        // File has a side effect tracked via an atom
2027        std::fs::write(
2028            dir.join("counter.cljrs"),
2029            "(ns counter) (def loaded-count (atom 0)) (swap! loaded-count inc)",
2030        )
2031        .unwrap();
2032        let (globals, mut env) = make_env_with_paths(vec![dir]);
2033        eval_src("(require 'counter)", &mut env).unwrap();
2034        eval_src("(require 'counter)", &mut env).unwrap();
2035        // The atom should have been incremented only once.
2036        let count = globals.lookup_in_ns("counter", "loaded-count").unwrap();
2037        if let Value::Atom(a) = count {
2038            assert_eq!(a.get().deref(), Value::Long(1));
2039        } else {
2040            panic!("expected atom");
2041        }
2042    }
2043
2044    #[test]
2045    fn test_require_not_found() {
2046        let (_, mut env) = make_env_with_paths(vec![]);
2047        let err = eval_src("(require 'nonexistent.ns)", &mut env).unwrap_err();
2048        let msg = format!("{err:?}");
2049        assert!(msg.contains("nonexistent.ns"), "unexpected error: {msg}");
2050    }
2051
2052    #[test]
2053    fn test_require_circular() {
2054        let dir = temp_ns_dir("require_circular");
2055        // a requires b, b requires a
2056        std::fs::write(dir.join("cira.cljrs"), "(ns cira (:require [cirb]))").unwrap();
2057        std::fs::write(dir.join("cirb.cljrs"), "(ns cirb (:require [cira]))").unwrap();
2058        let (_, mut env) = make_env_with_paths(vec![dir]);
2059        let err = eval_src("(require 'cira)", &mut env).unwrap_err();
2060        let msg = format!("{err:?}");
2061        assert!(
2062            msg.contains("circular"),
2063            "expected circular error, got: {msg}"
2064        );
2065    }
2066
2067    #[test]
2068    fn test_load_file() {
2069        let dir = temp_ns_dir("load_file");
2070        let path = dir.join("script.cljrs");
2071        std::fs::write(&path, "(+ 1 2)").unwrap();
2072        let (_, mut env) = make_env_with_paths(vec![]);
2073        let result = eval_src(&format!("(load-file \"{}\")", path.display()), &mut env).unwrap();
2074        assert_eq!(result, Value::Long(3));
2075    }
2076
2077    // ── *ns* and namespace reflection ─────────────────────────────────────────
2078
2079    #[test]
2080    fn test_star_ns_initial() {
2081        // After standard_env(), *ns* should be the user namespace.
2082        let (_, mut env) = make_env();
2083        let v = eval_src("*ns*", &mut env).unwrap();
2084        match v {
2085            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "user"),
2086            other => panic!("expected Namespace, got {other:?}"),
2087        }
2088    }
2089
2090    #[test]
2091    fn test_star_ns_after_in_ns() {
2092        let (_, mut env) = make_env();
2093        eval_src("(in-ns 'myns)", &mut env).unwrap();
2094        let v = eval_src("*ns*", &mut env).unwrap();
2095        match v {
2096            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "myns"),
2097            other => panic!("expected Namespace, got {other:?}"),
2098        }
2099    }
2100
2101    #[test]
2102    fn test_star_ns_after_ns_form() {
2103        let (_, mut env) = make_env();
2104        eval_src("(ns mytest.ns)", &mut env).unwrap();
2105        let v = eval_src("*ns*", &mut env).unwrap();
2106        match v {
2107            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "mytest.ns"),
2108            other => panic!("expected Namespace, got {other:?}"),
2109        }
2110    }
2111
2112    #[test]
2113    fn test_ns_name() {
2114        let (_, mut env) = make_env();
2115        let v = eval_src("(ns-name *ns*)", &mut env).unwrap();
2116        match v {
2117            Value::Symbol(s) => assert_eq!(s.get().name.as_ref(), "user"),
2118            other => panic!("expected Symbol, got {other:?}"),
2119        }
2120    }
2121
2122    #[test]
2123    fn test_find_ns() {
2124        let (_, mut env) = make_env();
2125        // known ns
2126        let v = eval_src("(find-ns 'user)", &mut env).unwrap();
2127        assert!(matches!(v, Value::Namespace(_)));
2128        // unknown ns
2129        let v2 = eval_src("(find-ns 'nonexistent)", &mut env).unwrap();
2130        assert_eq!(v2, Value::Nil);
2131    }
2132
2133    #[test]
2134    fn test_all_ns() {
2135        let (_, mut env) = make_env();
2136        let v = eval_src("(all-ns)", &mut env).unwrap();
2137        // Should be a list containing at least user and clojure.core
2138        let names: Vec<String> = match &v {
2139            Value::List(l) => l
2140                .get()
2141                .iter()
2142                .filter_map(|ns| match ns {
2143                    Value::Namespace(n) => Some(n.get().name.as_ref().to_string()),
2144                    _ => None,
2145                })
2146                .collect(),
2147            other => panic!("expected list, got {other:?}"),
2148        };
2149        assert!(names.contains(&"user".to_string()));
2150        assert!(names.contains(&"clojure.core".to_string()));
2151    }
2152
2153    #[test]
2154    fn test_ns_interns() {
2155        let (_, mut env) = make_env();
2156        eval_src("(def my-test-var 42)", &mut env).unwrap();
2157        let v = eval_src("(ns-interns *ns*)", &mut env).unwrap();
2158        let Value::Map(m) = v else {
2159            panic!("expected map")
2160        };
2161        // The map should contain 'my-test-var
2162        let sym = Value::symbol(cljrs_value::Symbol::simple("my-test-var"));
2163        assert!(m.get(&sym).is_some());
2164    }
2165
2166    #[test]
2167    fn test_create_ns() {
2168        let (_, mut env) = make_env();
2169        let v = eval_src("(create-ns 'fresh.ns)", &mut env).unwrap();
2170        match v {
2171            Value::Namespace(ns) => assert_eq!(ns.get().name.as_ref(), "fresh.ns"),
2172            other => panic!("expected Namespace, got {other:?}"),
2173        }
2174        // find-ns should now find it
2175        let v2 = eval_src("(find-ns 'fresh.ns)", &mut env).unwrap();
2176        assert!(matches!(v2, Value::Namespace(_)));
2177    }
2178
2179    // ── Dynamic variables (Phase 9) ───────────────────────────────────────────
2180
2181    #[test]
2182    fn test_dynamic_var_basic() {
2183        let (globals, mut env) = make_env();
2184        let result = eval_src("(def ^:dynamic *x* 10) (binding [*x* 42] *x*)", &mut env).unwrap();
2185        assert_eq!(result, Value::Long(42));
2186        // verify root is still bound
2187        let root = globals.lookup_in_ns("user", "*x*");
2188        assert_eq!(root, Some(Value::Long(10)));
2189    }
2190
2191    #[test]
2192    fn test_dynamic_var_restore() {
2193        let (_globals, mut env) = make_env();
2194        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2195        eval_src("(binding [*x* 42] *x*)", &mut env).unwrap();
2196        // After binding block, value restored to root
2197        let val = eval_src("*x*", &mut env).unwrap();
2198        assert_eq!(val, Value::Long(10));
2199    }
2200
2201    #[test]
2202    fn test_dynamic_var_nested() {
2203        let (_, mut env) = make_env();
2204        eval_src("(def ^:dynamic *x* 1)", &mut env).unwrap();
2205        let result = eval_src("(binding [*x* 2] (binding [*x* 3] *x*))", &mut env).unwrap();
2206        assert_eq!(result, Value::Long(3));
2207        // After both blocks
2208        let val = eval_src("*x*", &mut env).unwrap();
2209        assert_eq!(val, Value::Long(1));
2210    }
2211
2212    #[test]
2213    fn test_dynamic_var_unaffected() {
2214        let (_, mut env) = make_env();
2215        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2216        eval_src("(def y 99)", &mut env).unwrap();
2217        eval_src("(binding [*x* 42] *x*)", &mut env).unwrap();
2218        // non-dynamic var y is unchanged
2219        let val = eval_src("y", &mut env).unwrap();
2220        assert_eq!(val, Value::Long(99));
2221    }
2222
2223    #[test]
2224    #[ignore = "clojure.core/future is undefined by design; the experimental \
2225                stand-in does not convey dynamic bindings to its task"]
2226    fn test_binding_conveyance() {
2227        let (_, mut env) = make_env();
2228        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2229        let result = eval_src("(binding [*x* 42] @(future *x*))", &mut env).unwrap();
2230        assert_eq!(result, Value::Long(42));
2231    }
2232
2233    #[test]
2234    fn test_var_set_in_binding() {
2235        let (_, mut env) = make_env();
2236        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2237        // set! inside binding sets thread-local
2238        let inside = eval_src("(binding [*x* 1] (set! *x* 2) *x*)", &mut env).unwrap();
2239        assert_eq!(inside, Value::Long(2));
2240        // root still 10
2241        let root = eval_src("*x*", &mut env).unwrap();
2242        assert_eq!(root, Value::Long(10));
2243    }
2244
2245    #[test]
2246    fn test_with_bindings_star() {
2247        let (_, mut env) = make_env();
2248        eval_src("(def ^:dynamic *x* 10)", &mut env).unwrap();
2249        let result = eval_src("(with-bindings* {#'*x* 99} (fn [] *x*))", &mut env).unwrap();
2250        assert_eq!(result, Value::Long(99));
2251    }
2252
2253    #[test]
2254    fn test_binding_fully_qualified_cross_ns_dynamic_var() {
2255        let (_, mut env) = make_env();
2256        let result = eval_src(
2257            r#"
2258            (ns other.ns)
2259            (def ^:dynamic *dispatch* nil)
2260            (ns user)
2261            (binding [other.ns/*dispatch* (fn [x] x)]
2262              (other.ns/*dispatch* 42))
2263            "#,
2264            &mut env,
2265        )
2266        .unwrap();
2267        assert_eq!(result, Value::Long(42));
2268    }
2269
2270    #[test]
2271    fn test_binding_aliased_cross_ns_dynamic_var() {
2272        // (binding [alias/*var* v] ...) must resolve `alias` through the
2273        // current ns's `:require :as` aliases, exactly like ordinary
2274        // qualified-symbol lookup — this is how Replicant's public
2275        // `set-dispatch!`/life-cycle dispatch binds `*dispatch*` across
2276        // namespaces.
2277        let dir = temp_ns_dir("binding_aliased_cross_ns_dynamic_var");
2278        std::fs::create_dir_all(dir.join("replicant")).unwrap();
2279        std::fs::write(
2280            dir.join("replicant").join("core.cljrs"),
2281            r#"(ns replicant.core)
2282               (def ^:dynamic *dispatch* nil)
2283               (defn call-dispatch [x] (*dispatch* x))"#,
2284        )
2285        .unwrap();
2286        let (_, mut env) = make_env_with_paths(vec![dir]);
2287        let result = eval_src(
2288            r#"
2289            (ns life-cycle-test
2290              (:require [replicant.core :as r]))
2291            (binding [r/*dispatch* (fn [x] x)]
2292              (r/call-dispatch 42))
2293            "#,
2294            &mut env,
2295        )
2296        .unwrap();
2297        assert_eq!(result, Value::Long(42));
2298    }
2299
2300    #[test]
2301    fn test_meta_on_var() {
2302        let (_, mut env) = make_env();
2303        eval_src("(def ^:dynamic *x* 1)", &mut env).unwrap();
2304        let m = eval_src("(meta #'*x*)", &mut env).unwrap();
2305        // meta should be {:dynamic true}
2306        if let Value::Map(mv) = &m {
2307            let kw = Value::keyword(cljrs_value::Keyword::parse("dynamic"));
2308            assert_eq!(mv.get(&kw), Some(Value::Bool(true)));
2309        } else {
2310            panic!("expected map, got {m:?}");
2311        }
2312    }
2313
2314    #[test]
2315    fn test_bound_pred() {
2316        let (_, mut env) = make_env();
2317        eval_src("(def ^:dynamic *x* 1)", &mut env).unwrap();
2318        let t = eval_src("(bound? #'*x*)", &mut env).unwrap();
2319        assert_eq!(t, Value::Bool(true));
2320    }
2321
2322    #[test]
2323    fn test_alter_var_root() {
2324        let (_, mut env) = make_env();
2325        eval_src("(def x 1)", &mut env).unwrap();
2326        eval_src("(alter-var-root #'x inc)", &mut env).unwrap();
2327        let val = eval_src("x", &mut env).unwrap();
2328        assert_eq!(val, Value::Long(2));
2329    }
2330
2331    // ── clojure.test ─────────────────────────────────────────────────────────
2332
2333    #[test]
2334    fn test_clojure_test_is_pass() {
2335        // (is expr) returns true on a passing assertion.
2336        let (_, mut env) = make_env();
2337        eval_src(
2338            "(require '[clojure.test :refer [is deftest run-tests]])",
2339            &mut env,
2340        )
2341        .unwrap();
2342        let v = eval_src("(is (= 1 1))", &mut env).unwrap();
2343        assert_eq!(v, Value::Bool(true));
2344    }
2345
2346    #[test]
2347    fn test_clojure_test_is_fail() {
2348        // (is expr) returns false on a failing assertion.
2349        let (_, mut env) = make_env();
2350        eval_src("(require '[clojure.test :refer [is]])", &mut env).unwrap();
2351        let v = eval_src("(is (= 1 2))", &mut env).unwrap();
2352        assert_eq!(v, Value::Bool(false));
2353    }
2354
2355    #[test]
2356    fn test_clojure_test_is_catch_error() {
2357        // (is expr) catches runtime errors and returns false.
2358        let (_, mut env) = make_env();
2359        eval_src("(require '[clojure.test :refer [is]])", &mut env).unwrap();
2360        let v = eval_src("(is (/ 1 0))", &mut env).unwrap();
2361        assert_eq!(v, Value::Bool(false));
2362    }
2363
2364    #[test]
2365    fn test_clojure_test_deftest_and_run() {
2366        // deftest + run-tests smoke test: counters reflect pass/fail.
2367        let (_, mut env) = make_env();
2368        eval_src(
2369            "(require '[clojure.test :refer [deftest is run-tests]])",
2370            &mut env,
2371        )
2372        .unwrap();
2373        eval_src("(deftest my-passing-test (is (= 1 1)))", &mut env).unwrap();
2374        eval_src("(deftest my-failing-test (is (= 1 2)))", &mut env).unwrap();
2375        let counters = eval_src("(run-tests)", &mut env).unwrap();
2376        // Should have run 2 tests, 1 pass, 1 fail.
2377        if let Value::Map(m) = counters {
2378            let get = |k: &str| {
2379                m.get(&Value::keyword(cljrs_value::Keyword {
2380                    namespace: None,
2381                    name: Arc::from(k),
2382                }))
2383            };
2384            assert_eq!(get("test"), Some(Value::Long(2)));
2385            assert_eq!(get("pass"), Some(Value::Long(1)));
2386            assert_eq!(get("fail"), Some(Value::Long(1)));
2387            assert_eq!(get("error"), Some(Value::Long(0)));
2388        } else {
2389            panic!("expected map from run-tests, got {counters:?}");
2390        }
2391    }
2392
2393    #[test]
2394    fn test_alter_meta_bang() {
2395        // alter-meta! applies fn to var's meta and stores result.
2396        let (_, mut env) = make_env();
2397        eval_src("(def myvar 42)", &mut env).unwrap();
2398        eval_src("(alter-meta! #'myvar assoc :foo :bar)", &mut env).unwrap();
2399        let m = eval_src("(meta #'myvar)", &mut env).unwrap();
2400        if let Value::Map(map) = m {
2401            let foo_key = Value::keyword(cljrs_value::Keyword {
2402                namespace: None,
2403                name: Arc::from("foo"),
2404            });
2405            assert!(map.get(&foo_key).is_some());
2406        } else {
2407            panic!("expected map, got {m:?}");
2408        }
2409    }
2410
2411    #[test]
2412    fn test_catch_runtime_error() {
2413        // (try (/ 1 0) (catch Exception e "caught")) => "caught"
2414        let (_, mut env) = make_env();
2415        let v = eval_src(r#"(try (/ 1 0) (catch Exception e "caught"))"#, &mut env).unwrap();
2416        assert_eq!(v, Value::string("caught".to_string()));
2417    }
2418
2419    #[test]
2420    fn test_ns_resolve() {
2421        let (_, mut env) = make_env();
2422        eval_src("(def somevar 99)", &mut env).unwrap();
2423        // ns-resolve with current ns returns the var.
2424        let v = eval_src("(ns-resolve *ns* 'somevar)", &mut env).unwrap();
2425        assert!(matches!(v, Value::Var(_)));
2426        // ns-resolve for non-existent symbol returns nil.
2427        let v2 = eval_src("(ns-resolve *ns* 'nonexistent)", &mut env).unwrap();
2428        assert_eq!(v2, Value::Nil);
2429    }
2430
2431    // ── Persistent structure virtualization ──────────────────────────────
2432
2433    #[test]
2434    fn test_assoc_chain_virtualized() {
2435        // Assoc chain where intermediates aren't used — should be virtualized.
2436        let v = eval_str(
2437            "(let [m {}
2438                   a (assoc m :x 1)
2439                   b (assoc a :y 2)
2440                   c (assoc b :z 3)]
2441               c)",
2442        )
2443        .unwrap();
2444        // Result should be {:x 1, :y 2, :z 3}.
2445        assert!(matches!(&v, Value::Map(_)));
2446        if let Value::Map(m) = &v {
2447            assert_eq!(m.count(), 3);
2448            assert_eq!(m.get(&Value::keyword(Keyword::simple("x"))), Some(long(1)));
2449            assert_eq!(m.get(&Value::keyword(Keyword::simple("y"))), Some(long(2)));
2450            assert_eq!(m.get(&Value::keyword(Keyword::simple("z"))), Some(long(3)));
2451        }
2452    }
2453
2454    #[test]
2455    fn test_conj_chain_virtualized() {
2456        // Conj chain on a vector.
2457        let v = eval_str(
2458            "(let [v [1]
2459                   a (conj v 2)
2460                   b (conj a 3)
2461                   c (conj b 4)]
2462               c)",
2463        )
2464        .unwrap();
2465        assert_eq!(v, eval_str("[1 2 3 4]").unwrap());
2466    }
2467
2468    #[test]
2469    fn test_assoc_chain_intermediate_used_no_virtualize() {
2470        // If an intermediate is used in the body, virtualization should not apply,
2471        // but the result should still be correct.
2472        let v = eval_str(
2473            "(let [a (assoc {} :x 1)
2474                   b (assoc a :y 2)]
2475               (list (count a) (count b)))",
2476        )
2477        .unwrap();
2478        // a has 1 entry, b has 2.
2479        if let Value::List(l) = &v {
2480            let items: Vec<_> = l.get().iter().cloned().collect();
2481            assert_eq!(items, vec![long(1), long(2)]);
2482        } else {
2483            panic!("expected list, got {:?}", v);
2484        }
2485    }
2486
2487    #[test]
2488    fn test_assoc_chain_on_existing_map() {
2489        // Chain on an existing non-empty map.
2490        let v = eval_str(
2491            "(let [m {:a 1}
2492                   a (assoc m :b 2)
2493                   b (assoc a :c 3)]
2494               b)",
2495        )
2496        .unwrap();
2497        if let Value::Map(m) = &v {
2498            assert_eq!(m.count(), 3);
2499        } else {
2500            panic!("expected map");
2501        }
2502    }
2503
2504    // ── :pre/:post conditions ─────────────────────────────────────────────
2505
2506    #[test]
2507    fn test_post_condition_percent_bound() {
2508        // % must resolve to the return value inside :post conditions.
2509        let v = eval_str("(defn g [x] {:post [(pos? %)]} (inc x)) (g 5)").unwrap();
2510        assert_eq!(v, long(6));
2511    }
2512
2513    #[test]
2514    fn test_post_condition_violation_throws() {
2515        // A failing :post condition must throw.
2516        let r = eval_str("(defn g [x] {:post [(neg? %)]} (inc x)) (g 5)");
2517        assert!(r.is_err(), "expected error from failing :post condition");
2518    }
2519
2520    #[test]
2521    fn test_pre_condition_passes() {
2522        let v = eval_str("(defn g [x] {:pre [(pos? x)]} (inc x)) (g 5)").unwrap();
2523        assert_eq!(v, long(6));
2524    }
2525
2526    #[test]
2527    fn test_pre_condition_violation_throws() {
2528        let r = eval_str("(defn g [x] {:pre [(pos? x)]} (inc x)) (g -1)");
2529        assert!(r.is_err(), "expected error from failing :pre condition");
2530    }
2531
2532    #[test]
2533    fn test_pre_and_post_conditions() {
2534        let v = eval_str("(defn g [x] {:pre [(pos? x)] :post [(> % x)]} (inc x)) (g 3)").unwrap();
2535        assert_eq!(v, long(4));
2536    }
2537
2538    #[test]
2539    fn test_post_condition_no_pre() {
2540        // :post only (no :pre).
2541        let v = eval_str("(defn h [x] {:post [(number? %)]} (inc x)) (h 2)").unwrap();
2542        assert_eq!(v, long(3));
2543    }
2544
2545    #[test]
2546    fn test_pre_condition_no_post() {
2547        // :pre only (no :post); existing test variant without conditions map.
2548        let v = eval_str("(defn h [x] {:pre [(number? x)]} x) (h 42)").unwrap();
2549        assert_eq!(v, long(42));
2550    }
2551}