Skip to main content

cljrs_runtime/interp/
eval.rs

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