Lines
99.22 %
Functions
100 %
Branches
// Skipped under Miri: these tests compile+run wasm via wasmtime, whose
// Cranelift backend refuses to run under Miri.
#![cfg(not(miri))]
use nms::interpreter::Interpreter;
use scripting::nomiscript::{Fraction, Value};
#[test]
fn test_eval_defun_sum() {
let mut interp = Interpreter::new(false).unwrap();
let results = interp
.eval("(defun sum (a b c) \"Sums A, B, C\" (+ a b c))\n(sum 1 2 3)")
.unwrap();
assert_eq!(results, vec![Value::Number(Fraction::from_integer(6))]);
}
fn test_eval_defun_add() {
.eval("(defun add (a b) (+ a b))\n(add 10 20)")
assert_eq!(results, vec![Value::Number(Fraction::from_integer(30))]);
fn test_eval_defun_only() {
let results = interp.eval("(defun foo (x) (+ x 1))").unwrap();
assert_eq!(results, vec![Value::Symbol("FOO".to_string())]);
// --- Recursion depth guards (Gap B residue, #59) ---
// Both the eval-time inline walk (`SymbolTable::enter_inline`) and the codegen
// inline walk (`CompileContext::push_inlining_frame`) bound nested defun
// inlining so an unbounded / non-terminating recursion becomes a structured
// compile error instead of a native compiler-stack overflow (SIGABRT).
fn test_recursive_runtime_arg_defun_nested_in_native_does_not_overflow() {
// `(loopx i)` never reduces (passes its arg straight through), so the
// eval-time inline walk must recognize the runtime arg and bail to a
// runtime placeholder rather than recurse the compiler stack forever.
// Under ADR-0028 the surrounding `(+ 1 idx)` is now valid Index arithmetic
// (`transaction-tag-count` is a count; the old "requires ratio values, not
// indices" rejection is gone), so the form compiles to finite self-
// recursive wasm and the non-termination surfaces as a RUNTIME trap — not a
// compiler SIGABRT. The invariant: we reach this assertion with an `Err` at
// all, proving the compiler terminated.
let err = interp
.eval("(defun loopx (x) (loopx x)) (+ 1 (loopx (transaction-tag-count 0)))")
.expect_err("non-terminating recursive runtime-arg call must error, not overflow");
assert!(!err.to_string().is_empty());
fn test_recursive_runtime_arg_defun_in_effect_position_does_not_overflow() {
// Same non-terminating recursion but in EFFECT position (a non-tail
// statement whose value is discarded). The effect-position codegen has its
// own defun-inline path, which must also route to the monomorph path /
// depth-guard rather than recurse the compiler stack. It compiles, so the
// infinite recursion surfaces at RUNTIME (a wasm trap), not as a compiler
// SIGABRT.
.eval("(defun loopx (x) (loopx x)) (loopx (transaction-tag-count 0)) 1")
.expect_err("non-terminating recursion in effect position must not overflow the compiler");
// A clean structured/runtime error string, not a process abort.
fn test_deep_const_recursion_errors_cleanly_not_overflow() {
// Deep const-folded recursion drives the CODEGEN inline walk
// (`compile_lambda_call` → `push_inlining_frame`). Past the depth ceiling
// it must be a structured error, not a native stack overflow.
.eval("(defun cnt (n) (if (<= n 0) 0 (cnt (- n 1)))) (cnt 200)")
.expect_err("over-deep const recursion must error, not overflow");
assert!(
err.to_string().contains("inlining exceeded depth"),
"expected inlining-depth error, got: {err}"
);
fn test_recursive_runtime_arg_defun_nested_routes_to_monomorph() {
// The positive side of #59: a LEGITIMATE recursive defun over a runtime
// ratio arg, nested as a native argument, must route to the codegen
// monomorph runtime-call path (not overflow, not error). `dn` counts a
// runtime ratio down to 0; `(+ 1 (dn …))` compiles and the recursion runs
// at runtime. The eval-time placeholder for the re-entrant call is what
// hands the real lowering to the monomorph emit.
assert_eq!(
interp
.eval(
"(defun dn (n) (if (<= n (/ 1 2)) (/ 0 1) (dn (- n 1)))) \
(+ (/ 1 1) (dn (transaction-post-date 0)))"
)
.unwrap(),
vec![Value::Number(Fraction::from_integer(1))]
fn test_shallow_const_recursion_still_folds() {
// Recursion well within the depth ceiling must keep const-folding.
.eval("(defun fact (n) (if (<= n 1) 1 (* n (fact (- n 1))))) (fact 10)")
vec![Value::Number(Fraction::from_integer(3_628_800))]
.eval("(defun cnt (n) (if (<= n 0) 0 (cnt (- n 1)))) (cnt 20)")
vec![Value::Number(Fraction::from_integer(0))]
fn test_eval_defun_arity_error() {
let result = interp.eval("(defun add (a b) (+ a b))\n(add 1)");
assert!(result.is_err());
fn test_eval_defun_persistence() {
interp.eval("(defun add (a b) (+ a b))").unwrap();
let results = interp.eval("(add 3 4)").unwrap();
assert_eq!(results, vec![Value::Number(Fraction::from_integer(7))]);
fn test_eval_funcall_native() {
let results = interp.eval("(funcall + 1 2 3)").unwrap();
fn test_eval_funcall_user() {
.eval("(defun add (a b) (+ a b))\n(funcall add 10 20)")
fn test_eval_apply_quoted_list() {
let results = interp.eval("(apply + '(1 2 3))").unwrap();
fn test_eval_apply_mixed_args() {
let results = interp.eval("(apply + 1 2 '(3 4))").unwrap();
assert_eq!(results, vec![Value::Number(Fraction::from_integer(10))]);
fn test_eval_compile() {
.eval("(defun add (a b) (+ a b))\n(compile 'add)")
assert_eq!(results, vec![Value::Symbol("ADD".to_string())]);
fn test_eval_eval() {
let results = interp.eval("(eval '(+ 1 2))").unwrap();
assert_eq!(results, vec![Value::Number(Fraction::from_integer(3))]);
fn test_eval_eval_defun() {
let results = interp.eval("(eval '(defun x (a) (+ 1 a)))\n(x 5)").unwrap();
fn test_eval_eval_self_evaluating() {
interp.eval("(eval 42)").unwrap(),
vec![Value::Number(Fraction::from_integer(42))]
interp.eval("(eval \"hello\")").unwrap(),
vec![Value::String("hello".to_string())]
assert_eq!(interp.eval("(eval nil)").unwrap(), vec![Value::Nil]);
fn test_eval_eval_symbol() {
let results = interp.eval("(eval 'revision)").unwrap();
match &results[0] {
Value::String(s) => assert!(s.chars().all(|c| c.is_ascii_hexdigit())),
_ => panic!("expected string, got {:?}", results[0]),
fn test_eval_lambda_literal() {
let results = interp.eval("(lambda (x) (* x 2))").unwrap();
assert_eq!(results, vec![Value::String("<closure>".to_string())]);
fn test_eval_lambda_captures_renders_as_closure() {
let results = interp.eval("(let* ((y 3)) (lambda (x) (+ x y)))").unwrap();
fn test_eval_defun_then_call() {
.eval("(defun double (x) (* x 2)) (double 5)")
fn test_eval_eval_defun_dynamic() {
.eval("(eval '(defun sum (a) (+ a 1))) (sum 5)")
fn test_eval_funcall_lambda() {
let results = interp.eval("(funcall (lambda (x y) (+ x y)) 3 4)").unwrap();
fn test_eval_function_form() {
.eval("(defun f (x) (* x 2)) (funcall (function f) 5)")
fn test_funcall_let_bound_lambda() {
.eval("(let ((f (lambda (x) (* x 2)))) (funcall f 5))")
fn test_funcall_let_star_bound_lambda() {
.eval("(let* ((f (lambda (x) (* x 2)))) (funcall f 5))")
fn test_bare_operator_error() {
let err = interp.eval("+").unwrap_err();
err.to_string().contains("is a function"),
"expected function error, got: {err}"
fn test_bare_special_form_error() {
let err = interp.eval("quote").unwrap_err();
err.to_string().contains("is a special form"),
"expected special form error, got: {err}"
fn test_bare_native_function_error() {
let err = interp.eval("debug").unwrap_err();
fn test_labels_simple() {
.eval("(labels ((double (x) (* x 2))) (double 5))")
fn test_labels_factorial() {
"(defun fact (n)
(labels ((go (i acc)
(if (<= i 1) acc (go (- i 1) (* acc i)))))
(go n 1)))
(fact 5)",
assert_eq!(results, vec![Value::Number(Fraction::from_integer(120))]);
fn test_labels_mutual_recursion() {
"(labels ((is-even (n) (if (= n 0) t (is-odd (- n 1))))
(is-odd (n) (if (= n 0) nil (is-even (- n 1)))))
(is-even 4))",
assert_eq!(results, vec![Value::Bool(true)]);
fn test_labels_no_body_error() {
let err = interp.eval("(labels ((f (x) x)))").unwrap_err();
err.to_string().contains("LABELS requires"),
"expected labels error, got: {err}"
fn test_labels_scope() {
.eval("(labels ((local-fn (x) (+ x 1))) (local-fn 5))")
let err = interp.eval("(local-fn 5)").unwrap_err();
err.to_string().contains("undefined")
|| err.to_string().contains("Undefined")
|| err.to_string().contains("not defined"),
"expected undefined error, got: {err}"
fn test_1_plus() {
let results = interp.eval("(1+ 5)").unwrap();
fn test_1_minus() {
let results = interp.eval("(1- 5)").unwrap();
assert_eq!(results, vec![Value::Number(Fraction::from_integer(4))]);