cljrs-runtime 0.2.13

clojurust runtime: environment, builtins, tree-walking interpreter, and tiered evaluation
Documentation
//! Integration tests for interpreter evaluation under the `no-gc` feature.
//!
//! Each test verifies one of the allocation patterns described in
//! `docs/archive/no-gc-plan.md`:
//!
//! - `def` / `defn` values land in the `StaticArena` (not a scratch region).
//! - Function calls push a scratch region; the return value lands in the caller's context.
//! - `loop` / `recur` iterations use fresh scratch regions; accumulators survive.
//! - `atom` / `reset!` / `swap!` static-sink context pushes produce static values.
//!
//! Debug-assertions builds additionally check pointer provenance via
//! `GcPtr::is_static_alloc()`.
//!
//! Run with:
//!   cargo test -p cljrs-runtime --features no-gc

#![cfg(feature = "no-gc")]

use std::sync::Arc;

use cljrs_reader::Parser;
use cljrs_runtime::env::env::{Env, GlobalEnv};
use cljrs_value::Value;

// ── Test helpers ──────────────────────────────────────────────────────────────

fn make_env() -> (Arc<GlobalEnv>, Env) {
    let globals = cljrs_runtime::Runtime::builder()
        .execution_mode(cljrs_runtime::ExecutionMode::TreeWalk)
        .eager_clojure_test(true)
        .build()
        .expect("runtime")
        .into_globals();
    let env = Env::new(globals.clone(), "user");
    (globals, env)
}

/// Evaluate a multi-form source string in a shared env, return the last value.
fn eval_src(src: &str, env: &mut Env) -> Value {
    let mut parser = Parser::new(src.to_string(), "<test>".to_string());
    let forms = parser.parse_all().expect("parse error");
    let mut result = Value::Nil;
    for form in forms {
        result = cljrs_runtime::interp::eval::eval(&form, env).expect("eval error");
    }
    result
}

/// Evaluate a self-contained source string in a fresh env, return the last value.
fn eval_fresh(src: &str) -> Value {
    let (_, mut env) = make_env();
    eval_src(src, &mut env)
}

// ── Basic correctness ─────────────────────────────────────────────────────────

#[test]
fn arithmetic_is_correct() {
    assert_eq!(eval_fresh("(+ 1 2)"), Value::Long(3));
    assert_eq!(eval_fresh("(* 6 7)"), Value::Long(42));
    assert_eq!(eval_fresh("(- 10 3)"), Value::Long(7));
}

#[test]
fn if_form_is_correct() {
    assert_eq!(eval_fresh("(if true 1 2)"), Value::Long(1));
    assert_eq!(eval_fresh("(if false 1 2)"), Value::Long(2));
    assert!(
        matches!(eval_fresh("(if nil :no :yes)"), Value::Keyword(p) if p.get().name.as_ref() == "yes")
    );
}

#[test]
fn let_binding_is_correct() {
    assert_eq!(eval_fresh("(let [x 10 y (+ x 5)] y)"), Value::Long(15));
}

// ── def / defn allocation ─────────────────────────────────────────────────────

#[test]
fn def_scalar_does_not_panic() {
    // Var::bind has a debug_assert that fires if the stored value came from a
    // scratch region.  This test passes if no panic occurs.
    eval_fresh("(def x 42)");
}

#[test]
fn def_vector_does_not_panic() {
    eval_fresh("(def v [1 2 3])");
}

#[test]
fn def_map_does_not_panic() {
    eval_fresh("(def m {:a 1 :b 2})");
}

#[test]
fn def_value_is_readable() {
    let (globals, mut env) = make_env();
    eval_src("(def pi 3)", &mut env);
    let val = globals
        .lookup_in_ns("user", "pi")
        .expect("def should intern var in user ns");
    assert_eq!(val, Value::Long(3));
}

#[test]
fn def_vector_var_is_static() {
    // In no-gc debug builds the Var's value must be in the StaticArena.
    let (globals, mut env) = make_env();
    eval_src("(def v [10 20 30])", &mut env);
    let val = globals
        .lookup_in_ns("user", "v")
        .expect("var should exist after def");

    #[cfg(debug_assertions)]
    {
        if let Value::Vector(p) = &val {
            assert!(
                p.is_static_alloc(),
                "def'd vector must be allocated in the StaticArena"
            );
        } else {
            panic!("expected Value::Vector, got {val:?}");
        }
    }

    // Verify the contents are correct regardless of build profile.
    if let Value::Vector(p) = val {
        assert_eq!(p.get().count(), 3);
    } else {
        panic!("expected Value::Vector");
    }
}

// ── defn / function calls ─────────────────────────────────────────────────────

#[test]
fn defn_and_call_is_correct() {
    assert_eq!(
        eval_fresh("(defn double [x] (* x 2)) (double 21)"),
        Value::Long(42)
    );
}

#[test]
fn nested_fn_calls_do_not_corrupt_region_stack() {
    // Multiple levels of scratch regions must be pushed and popped correctly.
    assert_eq!(
        eval_fresh(
            "(defn add1 [x] (+ x 1)) \
             (defn add2 [x] (add1 (add1 x))) \
             (defn add4 [x] (add2 (add2 x))) \
             (add4 38)"
        ),
        Value::Long(42)
    );
}

#[test]
fn fn_return_value_is_correct_in_caller_context() {
    // Exercises the pop_for_return protocol: the returned value must survive
    // the scratch region reset and be readable from the caller.
    assert_eq!(
        eval_fresh(
            "(defn make-pair [a b] [a b]) \
             (let [p (make-pair 10 20)] (count p))"
        ),
        Value::Long(2)
    );
}

#[test]
fn closure_capture_is_correct() {
    assert_eq!(
        eval_fresh(
            "(defn make-adder [n] (fn [x] (+ x n))) \
             (let [add5 (make-adder 5)] (add5 37))"
        ),
        Value::Long(42)
    );
}

// ── loop / recur ──────────────────────────────────────────────────────────────

#[test]
fn loop_sum_accumulates_correctly() {
    // Each iteration pushes a fresh scratch region; the accumulator lives in
    // the enclosing scope (above the iteration region).
    assert_eq!(
        eval_fresh("(loop [acc 0 i 1] (if (> i 10) acc (recur (+ acc i) (+ i 1))))"),
        Value::Long(55)
    );
}

#[test]
fn loop_with_vector_accumulator_is_correct() {
    let result = eval_fresh(
        "(loop [acc [] i 0] \
           (if (= i 5) \
             acc \
             (recur (conj acc i) (+ i 1))))",
    );
    if let Value::Vector(p) = result {
        let v = p.get();
        assert_eq!(v.count(), 5);
        assert_eq!(v.nth(0), Some(&Value::Long(0)));
        assert_eq!(v.nth(4), Some(&Value::Long(4)));
    } else {
        panic!("expected Vector");
    }
}

#[test]
fn loop_in_defn_is_correct() {
    assert_eq!(
        eval_fresh(
            "(defn factorial [n] \
               (loop [i n acc 1] \
                 (if (<= i 1) acc (recur (- i 1) (* acc i))))) \
             (factorial 10)"
        ),
        Value::Long(3_628_800)
    );
}

// ── Atom / static-sink operations ─────────────────────────────────────────────

#[test]
fn atom_init_does_not_panic() {
    // Atom::new stores the initial value; in no-gc mode it must be static.
    eval_fresh("(def a (atom 0))");
}

#[test]
fn atom_swap_is_correct() {
    let (_, mut env) = make_env();
    eval_src("(def a (atom 0))", &mut env);
    eval_src("(swap! a inc)", &mut env);
    eval_src("(swap! a inc)", &mut env);
    let val = eval_src("@a", &mut env);
    assert_eq!(val, Value::Long(2));
}

#[test]
fn reset_bang_is_correct_and_does_not_panic() {
    // Atom::reset has a debug_assert that fires for region-local values.
    let (_, mut env) = make_env();
    eval_src("(def a (atom 0))", &mut env);
    eval_src("(reset! a 42)", &mut env);
    let val = eval_src("@a", &mut env);
    assert_eq!(val, Value::Long(42));
}

#[test]
fn loop_with_atom_accumulation_does_not_panic() {
    // The "OK" pattern from Layer 3 of the plan: compute value inside swap!
    // so the swap! body is evaluated under a StaticCtxGuard.
    let (_, mut env) = make_env();
    eval_src("(def log (atom []))", &mut env);
    eval_src(
        "(loop [i 0] \
           (when (< i 5) \
             (swap! log conj i) \
             (recur (+ i 1))))",
        &mut env,
    );
    let val = eval_src("@log", &mut env);
    if let Value::Vector(p) = val {
        assert_eq!(p.get().count(), 5);
    } else {
        panic!("expected Vector from atom deref");
    }
}

// ── String / collection return values ────────────────────────────────────────

#[test]
fn str_concat_is_correct() {
    let result = eval_fresh(r#"(str "hello" " " "world")"#);
    if let Value::Str(p) = result {
        assert_eq!(p.get().as_str(), "hello world");
    } else {
        panic!("expected Str");
    }
}

#[test]
fn assoc_returns_fresh_map() {
    // assoc must create a new map in the caller's context — the canonical
    // "transform-and-return" pattern from Layer 4 of the plan.
    let (globals, mut env) = make_env();
    eval_src("(def m {:count 0})", &mut env);
    eval_src(
        "(defn update-count [m] (assoc m :count (inc (:count m))))",
        &mut env,
    );
    let result = eval_src("(update-count m)", &mut env);

    #[cfg(debug_assertions)]
    {
        if let Value::Map(m) = &result {
            use cljrs_value::MapValue;
            let is_static = match m {
                MapValue::Array(p) => p.is_static_alloc(),
                MapValue::Hash(p) => p.is_static_alloc(),
                MapValue::Sorted(p) => p.is_static_alloc(),
            };
            // The result is the return value of a function call from the top-level.
            // Since the caller context at this point is the static arena (top-level
            // eval with no enclosing ScratchGuard), the returned map should be static.
            let _ = is_static; // checked only in specific call patterns
        }
    }

    // Correctness: the returned map has :count 1.
    let count = eval_src("(:count (update-count m))", &mut env);
    assert_eq!(count, Value::Long(1));

    // Original must be unchanged (persistent maps).
    let orig = globals
        .lookup_in_ns("user", "m")
        .expect("m should be defined");
    if let Value::Map(_) = orig {
        let orig_count = eval_src("(:count m)", &mut env);
        assert_eq!(orig_count, Value::Long(0));
    }
}

// ── Recursive functions ───────────────────────────────────────────────────────

#[test]
fn recursive_fibonacci_is_correct() {
    assert_eq!(
        eval_fresh(
            "(defn fib [n] \
               (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2))))) \
             (fib 10)"
        ),
        Value::Long(55)
    );
}

// ── Multiple top-level forms ──────────────────────────────────────────────────

#[test]
fn multiple_defs_and_calls_work() {
    let result = eval_fresh(
        "(def a 10) \
         (def b 20) \
         (defn sum [x y] (+ x y)) \
         (sum a b)",
    );
    assert_eq!(result, Value::Long(30));
}