Lines
99.13 %
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};
// LIST
#[test]
fn test_eval_list_empty() {
let mut interp = Interpreter::new(false).unwrap();
let results = interp.eval("(list)").unwrap();
assert_eq!(results, vec![Value::String("()".to_string())]);
}
fn test_eval_list_numbers() {
let results = interp.eval("(list 1 2 3)").unwrap();
assert_eq!(results, vec![Value::String("(1 2 3)".to_string())]);
fn test_eval_list_mixed() {
let results = interp.eval(r#"(list 1 "hello" #t)"#).unwrap();
assert_eq!(results, vec![Value::String("(1 hello #T)".to_string())]);
// CONS
fn test_eval_cons_onto_list() {
let results = interp.eval("(cons 1 (list 2 3))").unwrap();
fn test_eval_cons_onto_nil() {
let results = interp.eval("(cons 1 nil)").unwrap();
assert_eq!(results, vec![Value::String("(1)".to_string())]);
fn test_eval_cons_arity_error() {
assert!(interp.eval("(cons 1)").is_err());
assert!(interp.eval("(cons 1 2 3)").is_err());
fn test_eval_numeric_literal_in_list_stays_ratio() {
// A numeric literal is a dimensionless Ratio in EVERY context — including a
// pair cell — never an i32 count. Reclassifying an integer literal to I32
// when it rides a list cell was an illegal implicit Ratio→count conversion:
// the `5` came back from CAR as a count and arithmetic refused it. These
// exercise the RUNTIME pair path (a runtime element forces the chain
// runtime), so the literal `5` must survive as a usable Ratio.
// CAR of a runtime list holding `5` then arithmetic on it.
assert_eq!(
interp
.eval("(let* ((p (cons 5 (cons (transaction-post-date 0) nil)))) (+ (car p) 1))")
.unwrap(),
vec![Value::Number(Fraction::from_integer(6))]
);
// a mixed literal+runtime list no longer miscompiles to Bool; CAR is 5.
.eval("(car (list 5 (transaction-post-date 0)))")
vec![Value::Number(Fraction::from_integer(5))]
// CAR over a list whose element is a RUNTIME value (#57). `(list <runtime>)`
// used to embed the runtime placeholder inside a quoted compile-time list, so
// CAR extracted a bare placeholder with no stack value -> invalid wasm. Now
// `list` desugars a runtime element to the CONS chain, so every element type
// round-trips through the $pair stack path.
fn test_eval_car_of_list_with_runtime_i32() {
// tag-count is 0 on the minimal input; the i32 COUNT element keeps Number.
.eval("(car (list (transaction-tag-count 0)))")
vec![Value::Number(Fraction::from_integer(0))]
fn test_eval_car_of_list_with_runtime_ratio() {
.eval("(car (list (transaction-post-date 0)))")
fn test_eval_car_of_list_with_runtime_bool() {
// A bool element rides its own PairElement::Bool slot, so CAR recovers a
// truth value: (= 0 0) -> Bool(true), (< 0 0) -> Nil — NOT Number(1)/(0).
.eval("(car (list (= (transaction-tag-count 0) 0)))")
vec![Value::Bool(true)]
.eval("(car (list (< 0 (transaction-tag-count 0))))")
vec![Value::Nil]
fn test_eval_cdr_then_car_of_multi_runtime_list() {
// Two runtime bool elements; CDR then CAR reaches the second cell.
.eval(
"(car (cdr (list (= (transaction-tag-count 0) 0) \
(< 0 (transaction-tag-count 0)))))"
)
fn test_eval_do_accumulator_of_runtime_cons_car() {
// A `do` whose accumulator step is `(cons <host-fn-call> acc)` must size the
// accumulator local from the (eval-resolved) cons car's element type. The
// car is a not-yet-reduced `(transaction-… i)` List form; the pair-element
// walker now evals it on a clone instead of falling to the scalar default
// (which mis-sized `acc` and failed CONS cdr typing). Pre-existing bug
// surfaced while completing the PairElement::Bool work.
// i32 car: CAR of the accumulated list keeps Number.
"(car (do ((i 0 (+ i 1)) \
(acc nil (cons (transaction-tag-count 0) acc))) \
((= i 3) acc)))"
// bool car: CAR recovers a truth value (Bool), not Number.
(acc nil (cons (= (transaction-tag-count 0) 0) acc))) \
fn test_eval_runtime_list_applies_element_side_effects_once() {
// A runtime `(list …)` desugars to a CONS chain built from the
// already-resolved elements, NOT a re-eval of the source args — so a SETF
// side effect in an element runs exactly once. `n` ends at 1, not 2.
.eval("(let* ((n 0)) (list (transaction-tag-count 0) (setf n (+ n 1))) n)")
vec![Value::Number(Fraction::from_integer(1))]
fn test_eval_runtime_list_honors_shadowed_cons() {
// The runtime `(list …)` lowering builds its `$pair` chain by calling the
// CONS builder DIRECTLY, not by synthesizing a `(CONS …)` form — so a user
// `(defun cons …)` can't hijack list construction. With `cons` shadowed,
// `(list <runtime>)` still builds a real pair whose CAR is the element.
.eval("(defun cons (a b) 99) (car (list (transaction-tag-count 0)))")
fn test_eval_apply_honors_shadowed_list() {
// APPLY's `(list …)` spread fast-path must only fire for the BUILTIN list.
// A user `(defun list …)` shadows it (the call dispatcher gives a user
// function priority), so `(apply f (list 5))` must call the user `list`
// (→ 10), not spread `5`. 10 isn't a valid arg-list → structured error.
let mut shadowed = Interpreter::new(false).unwrap();
assert!(
shadowed
"(defun list (x) (* x 2)) (defun f (a) (+ a 100)) \
(apply (function f) (list 5))"
.is_err()
// Unshadowed (fresh interpreter — the defun above persists per-interp): the
// fast-path spreads → f(5) = 105.
let mut normal = Interpreter::new(false).unwrap();
normal
.eval("(defun f (a) (+ a 100)) (apply (function f) (list 5))")
vec![Value::Number(Fraction::from_integer(105))]
fn test_eval_apply_honors_same_call_list_rebind() {
// The builtin-`LIST` check for APPLY's spread fast-path must run AFTER the
// leading args evaluate (left-to-right), so a leading arg that rebinds
// `list` is visible. Here a `(defun list …)` leading arg shadows it, so the
// final `(list 5)` calls the user `list` (→ 10) and 10 isn't a valid
// arg-list → structured error, NOT a silent spread of `5`.
.eval("(defun f (a b) b) (apply (function f) (defun list (x) (* x 2)) (list 5))")
// QUOTE
fn test_eval_quote_number() {
let results = interp.eval("(quote 42)").unwrap();
assert_eq!(results, vec![Value::Number(Fraction::from_integer(42))]);
fn test_eval_quote_reader_sugar() {
let results = interp.eval("'42").unwrap();
fn test_eval_quote_string() {
let results = interp.eval("(quote \"hello\")").unwrap();
assert_eq!(results, vec![Value::String("hello".to_string())]);
// DESCRIBE
fn test_eval_describe_function() {
let results = interp
.eval(r#"(defun sum (a b) "Adds two numbers" (+ a b)) (describe sum)"#)
.unwrap();
match &results[0] {
Value::String(s) => {
assert!(s.contains("SUM is a function"), "got: {s}");
assert!(s.contains("Lambda:"), "got: {s}");
assert!(s.contains("Adds two numbers"), "got: {s}");
_ => panic!("expected string, got {:?}", results[0]),
fn test_eval_describe_variable() {
.eval(r#"(defvar *x* 42 "The answer") (describe *x*)"#)
assert!(s.contains("*X* is a variable"), "got: {s}");
assert!(s.contains("Value: 42"), "got: {s}");
assert!(s.contains("The answer"), "got: {s}");
// Nested calls
fn test_nested_comparison_with_subtraction() {
interp.eval("(= 1 (- 3 2))").unwrap(),
fn test_nested_addition_with_multiplication() {
interp.eval("(+ 1 (* 2 3))").unwrap(),
vec![Value::Number(Fraction::from_integer(7))]
fn test_nested_subtraction_multiple_args() {
interp.eval("(- 10 (+ 1 2) (- 5 2))").unwrap(),
vec![Value::Number(Fraction::from_integer(4))]
fn test_nested_addition_both_sides() {
interp.eval("(+ (+ 1 2) (+ 3 4))").unwrap(),
vec![Value::Number(Fraction::from_integer(10))]
fn test_deeply_nested_addition() {
interp.eval("(+ 1 (+ 2 (+ 3 4)))").unwrap(),
fn test_nested_calls_with_defun() {
.eval("(defun double (x) (* x 2)) (+ (double 3) (double 4))")
vec![Value::Number(Fraction::from_integer(14))]
fn test_nested_equal_with_arithmetic() {
interp.eval("(equal (+ 1 1) (- 4 2))").unwrap(),
fn test_nested_funcall_with_arithmetic() {
interp.eval("(funcall + (- 10 3) (* 2 3))").unwrap(),
vec![Value::Number(Fraction::from_integer(13))]