Lines
100 %
Functions
88.46 %
Branches
// Skipped under Miri: these tests compile+run wasm via wasmtime, whose
// Cranelift backend refuses to run under Miri.
#![cfg(not(miri))]
//! Regression tests for two IF / COND miscompiles surfaced while wiring
//! handler-case, both exercised end-to-end through script-mode `process`
//! (which `Interpreter::eval` drives) so the *value* is checked, not just
//! module validity:
//!
//! 1. **Let-bound runtime boolean branched at runtime.** A test that
//! resolves to `WasmLocal(_, I32)` (a let-bound comparison result) must
//! take the runtime `if` path, not const-fold to the then-branch. The
//! compile paths used to match only `WasmRuntime(I32)`, so the else
//! branch was unreachable — a wrong-branch miscompile that still
//! type-checked.
//! 2. **Bare top-level runtime IF / COND serialized once.** The
//! value-producing compile slot used to serialize *each branch*
//! independently, double-advancing the compile-time output cursor; only
//! one branch runs, so the decoder hit a garbage entity slot ("unknown
//! value type"). The fix collapses both arms to one stack value and
//! serializes a single result.
use nms::interpreter::Interpreter;
use scripting::nomiscript::{Fraction, Value};
fn eval_one(src: &str) -> Value {
let mut interp = Interpreter::new(false).unwrap();
interp
.eval(src)
.unwrap_or_else(|e| panic!("eval {src:?}: {e}"))
.into_iter()
.next_back()
.unwrap_or_else(|| panic!("eval {src:?} produced no value"))
}
fn n(v: i64) -> Value {
Value::Number(Fraction::from_integer(v))
#[test]
fn let_bound_runtime_boolean_takes_else_branch() {
// `p` is a let-bound comparison → WasmLocal(_, I32). n=1 so (= n 99) is
// false; the IF must branch to the else arm at runtime and yield "b".
// The pre-fix const-fold path always returned the then-branch "a".
assert_eq!(
eval_one(r#"(let* ((n 0)) (setf n 1) (let* ((p (= n 99))) (if p "a" "b")))"#),
Value::String("b".to_string())
);
fn let_bound_runtime_boolean_takes_then_branch() {
eval_one(r#"(let* ((n 0)) (setf n 1) (let* ((p (= n 1))) (if p "a" "b")))"#),
Value::String("a".to_string())
fn let_bound_runtime_boolean_cond_takes_else_branch() {
eval_one(r#"(let* ((n 0)) (setf n 1) (let* ((p (= n 99))) (cond (p "a") (t "b"))))"#),
fn bare_runtime_if_ratio_branches_serialize_once() {
// `(transaction-tag-count 0)` is a runtime i32; the comparison drives a
// bare top-level IF with ratio branches. Pre-fix this double-serialized
// and the decoder reported "unknown value type". tag-count is 0 on the
// minimal input, so the test is false → else arm → 6.
eval_one("(if (= (transaction-tag-count 0) 9999) 5 6)"),
n(6)
fn bare_runtime_if_string_branches_serialize_once() {
eval_one(r#"(if (= (transaction-tag-count 0) 9999) "a" "b")"#),
fn bare_runtime_if_bool_branches_select_false_arm() {
// The condition is false, so the `#f` arm is selected. The branches are
// booleans, so the result carries `WasmType::Bool` and the runtime
// serializer surfaces `#f` as `Nil` (matching the const-fold path), not
// `Number(0)`. This locks in the WasmType::Bool fix for the i32/bool
// serialization conflation.
eval_one("(if (= (transaction-tag-count 0) 9999) #t #f)"),
Value::Nil
fn bare_runtime_if_bool_branches_select_true_arm() {
// The condition is true (tag-count 0 == 0), so the `#t` arm is selected
// and serializes as `Bool(true)`, not `Number(1)`.
eval_one("(if (= (transaction-tag-count 0) 0) #t #f)"),
Value::Bool(true)
fn bare_runtime_comparison_serializes_as_bool() {
// A bare runtime comparison is a `WasmType::Bool`: a false result is `Nil`,
// not `Number(0)`.
assert_eq!(eval_one("(= (transaction-tag-count 0) 9999)"), Value::Nil);
eval_one("(= (transaction-tag-count 0) 0)"),
fn let_bound_if_with_bool_branches_via_runtime_prefix_block() {
// The eval-time type mirrors (let-body tail inference, block/handler peeks)
// must classify bool/nil literals as `Bool`, matching `compile_for_stack`.
// Here the inner `let` body has a runtime prefix (the comparison) before a
// `#t` tail, so the body's type is inferred via `infer_runtime_type`; if it
// returned I32 while codegen pushes Bool, the IF branches unify to AnyRef
// and the outer binding local mis-sizes. The condition is true, so `x` is
// `#t` → Bool(true).
eval_one(
"(let* ((x (if (= (transaction-tag-count 0) 0) \
(let* ((dummy (transaction-tag-count 0))) #t) \
#f))) \
x)"
),
fn runtime_if_with_empty_begin_branch_serializes_nil() {
// An empty `(begin)` ≡ nil. Its stack producer must report `WasmType::Bool`
// so it serializes as Nil (not Number(0)) and unifies homogeneously with
// the other Bool-typed `#t` branch of a runtime IF. tag-count is 0 → test
// true → the empty-begin arm → Nil.
eval_one("(if (= (transaction-tag-count 0) 0) (begin) #t)"),
fn no_else_runtime_if_with_count_then_keeps_number() {
// A no-else runtime IF whose then-branch is a raw i32 COUNT must keep
// Number fidelity. The missing else ≡ nil is typed `I32` (mirroring the
// count), so `unify_if_type` stays homogeneous (I32) instead of widening
// to AnyRef — which would box the count behind a non-null marker and lose
// its value. tag-count is 0 here, so the test is true → the count `0`.
eval_one("(if (= (transaction-tag-count 0) 0) (transaction-tag-count 0))"),
n(0)
// Test false → missing else fires → nil. An I32-typed nil serializes as
// the count's falsy form (Number(0)), not Bool.
eval_one("(if (= (transaction-tag-count 0) 9999) (transaction-tag-count 0))"),
fn no_else_runtime_if_with_bool_then_keeps_nil() {
// A no-else runtime IF whose then-branch is a Bool keeps Nil/Bool fidelity:
// the missing else ≡ nil is typed `Bool`. Test false → nil.
eval_one("(if (= (transaction-tag-count 0) 9999) (= (transaction-tag-count 0) 0))"),
fn bare_runtime_cond_serialize_once() {
eval_one("(cond ((= (transaction-tag-count 0) 9999) 5) (t 6))"),
fn tagbody_promoted_local_runtime_if_serializes_once() {
// The earlier "unknown value type" reproducer: a go-loop tagbody
// promotes `n` to a runtime local, then a value-position IF reads it.
"(let* ((n 0)) (tagbody loop (setf n (+ n 1)) (when (< n 1) (go loop))) \
(if (= n 99) 5 6))"
fn diverging_if_test_fires_instead_of_const_folding() {
// A `(return-from)` in IF test position transfers control before any
// condition is produced — the IF must NOT const-fold to a branch. The
// block yields the return-from value, never the then/else arms.
assert_eq!(eval_one("(block out (if (return-from out 7) 1 2))"), n(7));
fn diverging_if_test_with_dead_tail_after() {
eval_one("(block out (if (return-from out 7) 1 2) 99)"),
n(7)
fn diverging_cond_test_fires_instead_of_const_folding() {
eval_one("(block out (cond ((return-from out 7) 1)) 9)"),
fn diverging_cond_test_with_catch_all_clause() {
eval_one("(block out (cond ((return-from out 7) 1) (t 2)) 9)"),
fn cond_runtime_test_side_effect_runs_exactly_once() {
// The first clause test increments `n` and compares; its compile-time
// side effect must reach the symbol table exactly once (the suffix
// IF-chain is eval'd a single time). n ends at 1, so the catch-all
// returns 1 — not 2 (double-applied) and not an error.
eval_one("(let* ((n 0)) (cond ((begin (setf n (+ n 1)) (= n 99)) 1) (t n)))"),
n(1)
fn if_runtime_test_side_effect_runs_exactly_once() {
// setf in a runtime-test IF must apply once, not twice (classify on a
// clone, emit once). The runtime test compares against a host value.
"(let* ((n 0)) \
(if (begin (setf n (+ n 1)) (= (transaction-tag-count 0) n)) 0 0) n)"
fn cond_const_false_clause_between_runtime_clauses_in_effect_position() {
// Effect-position COND with a const-false clause (nil test) sitting
// between two runtime clauses. The open-guard-block count must track the
// runtime clauses actually emitted, not the raw clause index — else the
// closing `block_end`s over-count and the module fails to validate. The
// cond is a prefix (effect) form; the program returns the trailing X.
let wasm = interp
.compile_to_wasm(
"(cond ((= (transaction-tag-count 0) 1) (debug \"a\")) \
(nil (debug \"never\")) \
((= (transaction-tag-count 0) 2) (debug \"b\"))) \
7",
)
.unwrap_or_else(|e| panic!("compile: {e}"));
assert_eq!(interp.run_wasm(&wasm).unwrap(), n(7));