Lines
100 %
Functions
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::Value;
#[test]
fn test_eval_num_eq() {
let mut interp = Interpreter::new(false).unwrap();
assert_eq!(interp.eval("(= 1 1 1)").unwrap(), vec![Value::Bool(true)]);
assert_eq!(interp.eval("(= 1 2)").unwrap(), vec![Value::Nil]);
}
fn test_eval_num_eq_single() {
assert_eq!(interp.eval("(= 42)").unwrap(), vec![Value::Bool(true)]);
fn test_eval_num_neq() {
assert_eq!(interp.eval("(/= 1 2 3)").unwrap(), vec![Value::Bool(true)]);
assert_eq!(interp.eval("(/= 1 2 1)").unwrap(), vec![Value::Nil]);
fn test_eval_num_lt() {
assert_eq!(interp.eval("(< 1 2 3)").unwrap(), vec![Value::Bool(true)]);
assert_eq!(interp.eval("(< 1 2 2)").unwrap(), vec![Value::Nil]);
fn test_eval_num_gt() {
assert_eq!(interp.eval("(> 3 2 1)").unwrap(), vec![Value::Bool(true)]);
assert_eq!(interp.eval("(> 3 3 1)").unwrap(), vec![Value::Nil]);
fn test_eval_num_le() {
assert_eq!(
interp.eval("(<= 1 2 2 3)").unwrap(),
vec![Value::Bool(true)]
);
assert_eq!(interp.eval("(<= 1 3 2)").unwrap(), vec![Value::Nil]);
fn test_eval_num_ge() {
interp.eval("(>= 3 2 2 1)").unwrap(),
assert_eq!(interp.eval("(>= 1 2 3)").unwrap(), vec![Value::Nil]);
fn test_eval_comparison_type_error() {
assert!(interp.eval("(= 1 \"one\")").is_err());
assert!(interp.eval("(< \"a\" \"b\")").is_err());
fn test_eval_eql_numbers() {
assert_eq!(interp.eval("(eql 1 1)").unwrap(), vec![Value::Bool(true)]);
assert_eq!(interp.eval("(eql 1 2)").unwrap(), vec![Value::Nil]);
fn test_eval_eql_strings() {
interp.eval(r#"(eql "abc" "abc")"#).unwrap(),
interp.eval(r#"(eql "abc" "def")"#).unwrap(),
vec![Value::Nil]
fn test_eval_eql_mixed_types() {
assert_eq!(interp.eval(r#"(eql 1 "1")"#).unwrap(), vec![Value::Nil]);
fn test_eval_eql_nil() {
interp.eval("(eql nil nil)").unwrap(),
fn test_eval_eql_arity_error() {
assert!(interp.eval("(eql 1)").is_err());
assert!(interp.eval("(eql 1 2 3)").is_err());
fn test_eval_equal_strings() {
interp.eval(r#"(equal "hello" "hello")"#).unwrap(),
interp.eval(r#"(equal "hello" "world")"#).unwrap(),
fn test_eval_equal_numbers() {
interp.eval("(equal 0.5 0.5)").unwrap(),
fn test_eval_equal_arity_error() {
assert!(interp.eval("(equal 1)").is_err());
// --- Runtime bool serialization (WasmType::Bool) ---
// `(transaction-tag-count 0)` is a RUNTIME i32 (0 on the minimal input), so
// these comparisons/predicates can't const-fold — they exercise the runtime
// producers. The result must serialize as Bool/Nil (the falsy/truthy pair),
// not Number, mirroring the const-fold path above.
fn test_runtime_comparison_true_is_bool() {
interp.eval("(= (transaction-tag-count 0) 0)").unwrap(),
fn test_runtime_comparison_false_is_nil() {
interp.eval("(< 0 (transaction-tag-count 0))").unwrap(),
fn test_runtime_not_is_bool() {
// (not <runtime-false>) → true; tag-count is 0 (falsy) so not → Bool(true).
interp
.eval("(not (< 0 (transaction-tag-count 0)))")
.unwrap(),
fn test_runtime_and_or_are_bool() {
// and of two runtime comparisons: tag-count 0 == 0 (true) and 0 >= 0
// (true) → Bool(true).
.eval("(and (= (transaction-tag-count 0) 0) (>= (transaction-tag-count 0) 0))")
// or short-circuits on the first truthy runtime comparison. This case
// (true FIRST arg) is the regression: the old effect-only short-circuit
// helper produced no output entity on the early-exit branch, so a
// top-level `(or <true> …)` returned "no output entities". The
// value-producing stack path yields a result on every branch.
.eval("(or (= (transaction-tag-count 0) 0) (< (transaction-tag-count 0) 0))")
// false first, true second — exercises the short-circuit fall-through too.
.eval("(or (< (transaction-tag-count 0) 0) (= (transaction-tag-count 0) 0))")
fn test_unary_runtime_and_or_truthify_count_to_bool() {
// A unary `(and <count>)` / `(or <count>)` is a truth-value producer: it
// answers "is the count truthy". The runtime i32 count (0 on minimal
// input) must serialize as Nil (falsy), not Number(0) — the unary fast
// path truthifies the i32 operand to Bool, matching the multi-arg path.
interp.eval("(and (transaction-tag-count 0))").unwrap(),
interp.eval("(or (transaction-tag-count 0))").unwrap(),
fn test_unary_and_or_over_bound_count_local_truthify_to_bool() {
// The operand is a bound runtime LOCAL (`WasmLocal(_, I32)`), not a fresh
// placeholder. The eval-time mirror must treat it like codegen's
// `is_bool_runtime` (which matches both WasmRuntime and WasmLocal) and
// truthify it to Bool — else `x` aliases the raw count local and
// serializes as Number(0) instead of Nil.
.eval("(let* ((n (transaction-tag-count 0)) (x (and n))) x)")
.eval("(let* ((n (transaction-tag-count 0)) (x (or n))) x)")
fn test_effect_position_and_or_with_ref_runtime_operand_rejected() {
// A ref-typed runtime operand in EFFECT position must be REJECTED with a
// structured error (same contract as IF) — nomiscript does not do implicit
// truthiness for runtime refs (a runtime ref may be null, so treating it as
// always-truthy would silently mis-branch). NOT a malformed wasm module,
// NOT a silent truthy. `(transaction-post-date 0)` is a runtime Ratio.
let err = interp
.eval(r#"(and (transaction-post-date 0) 1) 7"#)
.expect_err("ref-typed and operand in effect position must error");
assert!(
err.to_string().contains("truth value"),
"expected a structured truth-value error, got: {err}"
.eval(r#"(or (transaction-post-date 0) 1) 7"#)
.expect_err("ref-typed or operand in effect position must error");
fn test_runtime_and_or_reject_ref_typed_operand() {
// A runtime `and`/`or` lowers each operand into an `if (result i32)` short-
// circuit block, so a ref-typed (StringRef/Ratio/…) operand can't ride it.
// It must be a structured compile error, NOT a malformed wasm module.
.eval(r#"(and (= (transaction-tag-count 0) 0) "x")"#)
.expect_err("ref-typed and operand must error");
err.to_string().to_lowercase().contains("truth value") || err.to_string().contains("AND"),
fn test_runtime_nullable_ref_in_and_or_rejected_use_null_predicate() {
// #58: a let-bound runtime PairRef that is nil at runtime must NOT be
// silently treated as truthy by `or`/`and` (the pre-fix bug returned wrong
// values). It is rejected with the same structured error as IF; the script
// must test nullability explicitly via `(null? …)`.
let nullable = "(let* ((x (cdr (cons (transaction-tag-count 0) nil)))) ";
.eval(&format!("{nullable} (or x 7))"))
.unwrap_err()
.to_string()
.contains("truth value")
.eval(&format!("{nullable} (and x 7))"))
// The `(null? …)` escape hatch yields a Bool, so the form compiles and the
// null ref reads as truthy-null: `(or (null? x) …)` → Bool(true).
.eval(&format!(
"{nullable} (or (null? x) (= (transaction-tag-count 0) 5)))"
))