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