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