use fusevm::{ChunkBuilder, Op, VMResult, Value, VM};
fn run(ops: &[(Op, u32)]) -> Value {
let mut b = ChunkBuilder::new();
for (op, line) in ops {
b.emit(op.clone(), *line);
}
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(val) => val,
VMResult::Halted => Value::Undef,
VMResult::Error(e) => panic!("unexpected VM error: {}", e),
}
}
fn run_int(ops: &[(Op, u32)]) -> i64 {
match run(ops) {
Value::Int(n) => n,
Value::Float(f) => f as i64,
other => panic!("expected Int, got {:?}", other),
}
}
fn run_float(ops: &[(Op, u32)]) -> f64 {
match run(ops) {
Value::Float(f) => f,
Value::Int(n) => n as f64,
other => panic!("expected Float, got {:?}", other),
}
}
fn run_bool(ops: &[(Op, u32)]) -> bool {
match run(ops) {
Value::Bool(b) => b,
other => panic!("expected Bool, got {:?}", other),
}
}
#[test]
fn integer_add_wraps_on_overflow() {
let n = run_int(&[
(Op::LoadInt(i64::MAX), 1),
(Op::LoadInt(1), 1),
(Op::Add, 1),
]);
assert_eq!(n, i64::MIN);
}
#[test]
fn integer_sub_wraps_on_underflow() {
let n = run_int(&[
(Op::LoadInt(i64::MIN), 1),
(Op::LoadInt(1), 1),
(Op::Sub, 1),
]);
assert_eq!(n, i64::MAX);
}
#[test]
fn integer_mul_wraps() {
let n = run_int(&[
(Op::LoadInt(i64::MAX), 1),
(Op::LoadInt(2), 1),
(Op::Mul, 1),
]);
assert_eq!(n, i64::MAX.wrapping_mul(2));
}
#[test]
fn div_by_zero_int_returns_undef() {
let v = run(&[(Op::LoadInt(42), 1), (Op::LoadInt(0), 1), (Op::Div, 1)]);
assert!(matches!(v, Value::Undef));
}
#[test]
fn mod_by_zero_int_returns_zero() {
let n = run_int(&[(Op::LoadInt(42), 1), (Op::LoadInt(0), 1), (Op::Mod, 1)]);
assert_eq!(n, 0);
}
#[test]
fn shift_amount_masked_to_low_6_bits() {
let n = run_int(&[(Op::LoadInt(1), 1), (Op::LoadInt(64), 1), (Op::Shl, 1)]);
assert_eq!(n, 1);
let n = run_int(&[(Op::LoadInt(1), 1), (Op::LoadInt(65), 1), (Op::Shl, 1)]);
assert_eq!(n, 2);
}
#[test]
fn float_arithmetic_with_nan() {
let v = run(&[
(Op::LoadFloat(f64::NAN), 1),
(Op::LoadFloat(1.0), 1),
(Op::Add, 1),
]);
match v {
Value::Float(f) => assert!(f.is_nan()),
other => panic!("expected NaN, got {:?}", other),
}
}
#[test]
fn float_arithmetic_infinity() {
let f = run_float(&[
(Op::LoadFloat(f64::INFINITY), 1),
(Op::LoadFloat(1.0), 1),
(Op::Add, 1),
]);
assert!(f.is_infinite() && f > 0.0);
}
#[test]
fn float_eq_with_nan_is_false() {
let b = run_bool(&[
(Op::LoadFloat(f64::NAN), 1),
(Op::LoadFloat(f64::NAN), 1),
(Op::NumEq, 1),
]);
assert!(!b);
}
#[test]
fn negate_min_int_wraps() {
let n = run_int(&[(Op::LoadInt(i64::MIN), 1), (Op::Negate, 1)]);
assert_eq!(n, i64::MIN);
}
#[test]
fn inc_max_int_wraps_to_min() {
let n = run_int(&[(Op::LoadInt(i64::MAX), 1), (Op::Inc, 1)]);
assert_eq!(n, i64::MIN);
}
#[test]
fn dec_min_int_wraps_to_max() {
let n = run_int(&[(Op::LoadInt(i64::MIN), 1), (Op::Dec, 1)]);
assert_eq!(n, i64::MAX);
}
#[test]
fn pow_with_negative_exp_returns_zero() {
let v = run(&[(Op::LoadInt(2), 1), (Op::LoadInt(-1), 1), (Op::Pow, 1)]);
if let Value::Float(f) = v {
assert!((f - 0.5).abs() < 1e-9);
} else {
panic!("expected Float, got {:?}", v);
}
}
#[test]
fn bool_coerces_to_int_in_arithmetic() {
let n = run_int(&[(Op::LoadTrue, 1), (Op::LoadInt(40), 1), (Op::Add, 1)]);
assert_eq!(n, 41);
}
#[test]
fn status_zero_is_truthy_shell_semantics() {
assert!(Value::Status(0).is_truthy());
assert!(!Value::Status(1).is_truthy());
assert!(!Value::Status(127).is_truthy());
}
#[test]
fn empty_string_is_falsy() {
assert!(!Value::str("").is_truthy());
assert!(!Value::str("0").is_truthy());
assert!(Value::str("00").is_truthy());
assert!(Value::str("0.0").is_truthy());
}
#[test]
fn empty_array_is_falsy() {
assert!(!Value::Array(vec![]).is_truthy());
assert!(Value::Array(vec![Value::Int(0)]).is_truthy());
}
#[test]
fn empty_hash_is_falsy() {
use std::collections::HashMap;
assert!(!Value::Hash(HashMap::new()).is_truthy());
let mut m = HashMap::new();
m.insert("k".to_string(), Value::Int(0));
assert!(Value::Hash(m).is_truthy());
}
#[test]
fn string_to_int_parses_or_zero() {
assert_eq!(Value::str("42").to_int(), 42);
assert_eq!(Value::str("-7").to_int(), -7);
assert_eq!(Value::str("not a number").to_int(), 0);
assert_eq!(Value::str("").to_int(), 0);
}
#[test]
fn array_to_int_returns_length() {
let arr = Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
assert_eq!(arr.to_int(), 3);
}
#[test]
fn as_str_cow_borrows_for_str_variant() {
use std::borrow::Cow;
let v = Value::str("hello");
let cow = v.as_str_cow();
assert!(matches!(cow, Cow::Borrowed(_)));
assert_eq!(&*cow, "hello");
}
#[test]
fn as_str_cow_borrows_for_bool_undef_hash() {
use std::borrow::Cow;
use std::collections::HashMap;
assert!(matches!(Value::Bool(true).as_str_cow(), Cow::Borrowed("1")));
assert!(matches!(Value::Bool(false).as_str_cow(), Cow::Borrowed("")));
assert!(matches!(Value::Undef.as_str_cow(), Cow::Borrowed("")));
assert!(matches!(
Value::Hash(HashMap::new()).as_str_cow(),
Cow::Borrowed("(hash)")
));
}
#[test]
fn as_str_cow_allocates_for_int_float() {
use std::borrow::Cow;
assert!(matches!(Value::Int(42).as_str_cow(), Cow::Owned(_)));
assert!(matches!(Value::Float(3.14).as_str_cow(), Cow::Owned(_)));
assert_eq!(&*Value::Int(42).as_str_cow(), "42");
assert_eq!(&*Value::Float(3.14).as_str_cow(), "3.14");
}
#[test]
fn pop_on_empty_stack_is_safe() {
let v = run(&[(Op::Pop, 1)]);
assert!(matches!(v, Value::Undef));
}
#[test]
fn dup_preserves_value() {
let n = run_int(&[
(Op::LoadInt(7), 1),
(Op::Dup, 1),
(Op::Add, 1), ]);
assert_eq!(n, 14);
}
#[test]
fn swap_exchanges_top_two() {
let n = run_int(&[
(Op::LoadInt(3), 1),
(Op::LoadInt(7), 1),
(Op::Swap, 1),
(Op::Sub, 1),
]);
assert_eq!(n, 4);
}
#[test]
fn rot_left_rotates_top_three() {
let n = run_int(&[
(Op::LoadInt(10), 1),
(Op::LoadInt(20), 1),
(Op::LoadInt(30), 1),
(Op::Rot, 1),
(Op::Add, 1), (Op::Add, 1), ]);
assert_eq!(n, 60);
}
#[test]
fn rot_with_only_two_elements_is_noop() {
let n = run_int(&[
(Op::LoadInt(5), 1),
(Op::LoadInt(10), 1),
(Op::Rot, 1),
(Op::Sub, 1), ]);
assert_eq!(n, -5);
}
#[test]
fn concat_int_and_string() {
let mut b = ChunkBuilder::new();
let s_idx = b.add_constant(Value::str(" items"));
b.emit(Op::LoadInt(42), 1);
b.emit(Op::LoadConst(s_idx), 1);
b.emit(Op::Concat, 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Str(s)) => assert_eq!(s.as_str(), "42 items"),
other => panic!("expected '42 items', got {:?}", other),
}
}
#[test]
fn concat_two_empty_strings() {
let mut b = ChunkBuilder::new();
let s_idx = b.add_constant(Value::str(""));
b.emit(Op::LoadConst(s_idx), 1);
b.emit(Op::LoadConst(s_idx), 1);
b.emit(Op::Concat, 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Str(s)) => assert!(s.is_empty()),
other => panic!("expected empty string, got {:?}", other),
}
}
#[test]
fn concat_undef_and_string_yields_string() {
let mut b = ChunkBuilder::new();
let s_idx = b.add_constant(Value::str("hello"));
b.emit(Op::LoadUndef, 1);
b.emit(Op::LoadConst(s_idx), 1);
b.emit(Op::Concat, 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Str(s)) => assert_eq!(s.as_str(), "hello"),
other => panic!("got {:?}", other),
}
}
#[test]
fn fused_and_unfused_sum_loops_match() {
fn fused_sum(n: i32) -> i64 {
let mut b = ChunkBuilder::new();
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::AccumSumLoop(0, 1, n), 1);
b.emit(Op::GetSlot(0), 1);
let mut vm = VM::new(b.build());
if let VMResult::Ok(Value::Int(n)) = vm.run() {
n
} else {
panic!()
}
}
fn unfused_sum(n: i64) -> i64 {
let mut b = ChunkBuilder::new();
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::GetSlot(0), 1);
b.emit(Op::GetSlot(1), 1);
b.emit(Op::Add, 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::PreIncSlotVoid(1), 1);
b.emit(Op::SlotLtIntJumpIfFalse(1, n as i32, 12), 1);
b.emit(Op::Jump(5), 1);
b.emit(Op::GetSlot(0), 1);
let mut vm = VM::new(b.build());
if let VMResult::Ok(Value::Int(n)) = vm.run() {
n
} else {
panic!()
}
}
for n in [10, 100, 1000, 10000] {
let expected = (n as i64) * (n as i64 - 1) / 2;
assert_eq!(fused_sum(n), expected, "fused mismatch at n={}", n);
assert_eq!(
unfused_sum(n as i64),
expected,
"unfused mismatch at n={}",
n
);
assert_eq!(fused_sum(n), unfused_sum(n as i64));
}
}
#[cfg(feature = "jit")]
#[test]
fn block_jit_matches_interpreter_on_loop() {
use fusevm::JitCompiler;
let mut b = ChunkBuilder::new();
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::AccumSumLoop(0, 1, 1000), 1);
b.emit(Op::GetSlot(0), 1);
let chunk = b.build();
let mut vm = VM::new(chunk.clone());
let interp = match vm.run() {
VMResult::Ok(Value::Int(n)) => n,
_ => panic!(),
};
let jit = JitCompiler::new();
let mut slots = vec![0i64; 4];
let jit_result = jit.try_run_block_eager(&chunk, &mut slots).unwrap();
assert_eq!(interp, 499500); assert_eq!(interp, jit_result);
}
#[test]
fn global_var_persists_after_set() {
let mut b = ChunkBuilder::new();
let x = b.add_name("x");
b.emit(Op::LoadInt(42), 1);
b.emit(Op::SetVar(x), 1);
b.emit(Op::GetVar(x), 1);
let mut vm = VM::new(b.build());
let _ = vm.run();
assert!(matches!(vm.globals[x as usize], Value::Int(42)));
}
#[test]
fn array_in_place_mutation_persists() {
let mut b = ChunkBuilder::new();
let arr = b.add_name("arr");
b.emit(Op::DeclareArray(arr), 1);
for i in 1..=5 {
b.emit(Op::LoadInt(i), 1);
b.emit(Op::ArrayPush(arr), 1);
}
b.emit(Op::LoadInt(0), 1); let mut vm = VM::new(b.build());
let _ = vm.run();
if let Value::Array(ref v) = vm.globals[arr as usize] {
assert_eq!(v.len(), 5);
assert_eq!(v[0], Value::Int(1));
assert_eq!(v[4], Value::Int(5));
} else {
panic!("expected array global");
}
}
#[test]
fn hash_in_place_mutation_persists() {
let mut b = ChunkBuilder::new();
let h = b.add_name("h");
let key1 = b.add_constant(Value::str("alpha"));
let key2 = b.add_constant(Value::str("beta"));
b.emit(Op::DeclareHash(h), 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::LoadConst(key1), 1);
b.emit(Op::HashSet(h), 1);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::LoadConst(key2), 1);
b.emit(Op::HashSet(h), 1);
b.emit(Op::LoadInt(0), 1);
let mut vm = VM::new(b.build());
let _ = vm.run();
if let Value::Hash(ref m) = vm.globals[h as usize] {
assert_eq!(m.len(), 2);
assert_eq!(m.get("alpha"), Some(&Value::Int(1)));
assert_eq!(m.get("beta"), Some(&Value::Int(2)));
} else {
panic!("expected hash global");
}
}
#[test]
fn last_status_tracked_across_set_get() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(127), 1);
b.emit(Op::SetStatus, 1);
b.emit(Op::GetStatus, 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Status(127)) => {}
other => panic!("expected Status(127), got {:?}", other),
}
assert_eq!(vm.last_status, 127);
}
#[test]
fn calling_unknown_function_returns_error() {
let mut b = ChunkBuilder::new();
let name = b.add_name("does_not_exist");
b.emit(Op::Call(name, 0), 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Error(msg) => assert!(msg.contains("does_not_exist")),
other => panic!("expected error, got {:?}", other),
}
}
#[test]
fn array_get_out_of_bounds_returns_undef() {
let mut b = ChunkBuilder::new();
let arr = b.add_name("arr");
b.emit(Op::DeclareArray(arr), 1);
b.emit(Op::LoadInt(7), 1); b.emit(Op::ArrayPush(arr), 1);
b.emit(Op::LoadInt(99), 1); b.emit(Op::ArrayGet(arr), 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Undef) => {}
other => panic!("expected Undef, got {:?}", other),
}
}
#[test]
fn hash_get_missing_key_returns_undef() {
let mut b = ChunkBuilder::new();
let h = b.add_name("h");
let key = b.add_constant(Value::str("missing"));
b.emit(Op::DeclareHash(h), 1);
b.emit(Op::LoadConst(key), 1);
b.emit(Op::HashGet(h), 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Undef) => {}
other => panic!("got {:?}", other),
}
}
#[test]
fn array_shift_on_empty_returns_undef() {
let mut b = ChunkBuilder::new();
let arr = b.add_name("arr");
b.emit(Op::DeclareArray(arr), 1);
b.emit(Op::ArrayShift(arr), 1);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Undef) => {}
other => panic!("got {:?}", other),
}
}
#[test]
fn function_can_return_early_with_returnvalue() {
let mut b = ChunkBuilder::new();
let fname = b.add_name("early");
b.emit(Op::LoadInt(0), 1);
b.emit(Op::Call(fname, 1), 1);
let main_end = b.emit(Op::Jump(0), 1);
let fn_ip = b.current_pos();
b.add_sub_entry(fname, fn_ip);
b.emit(Op::SetSlot(0), 2); b.emit(Op::GetSlot(0), 2);
b.emit(Op::LoadInt(0), 2);
b.emit(Op::NumEq, 2);
let else_jump = b.emit(Op::JumpIfFalse(0), 2);
b.emit(Op::LoadInt(99), 2);
b.emit(Op::ReturnValue, 2);
let else_ip = b.current_pos();
b.patch_jump(else_jump, else_ip);
b.emit(Op::LoadInt(-1), 2);
b.emit(Op::ReturnValue, 2);
let after = b.current_pos();
b.patch_jump(main_end, after);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Int(99)) => {}
other => panic!("expected 99, got {:?}", other),
}
}
#[test]
fn function_recursion_terminates_at_base() {
let mut b = ChunkBuilder::new();
let fname = b.add_name("fact");
b.emit(Op::LoadInt(5), 1);
b.emit(Op::Call(fname, 1), 1);
let main_end = b.emit(Op::Jump(0), 1);
let fn_ip = b.current_pos();
b.add_sub_entry(fname, fn_ip);
b.emit(Op::SetSlot(0), 2);
b.emit(Op::GetSlot(0), 2);
b.emit(Op::LoadInt(1), 2);
b.emit(Op::NumLe, 2);
let recurse_jump = b.emit(Op::JumpIfFalse(0), 2);
b.emit(Op::LoadInt(1), 2);
b.emit(Op::ReturnValue, 2);
let recurse_ip = b.current_pos();
b.patch_jump(recurse_jump, recurse_ip);
b.emit(Op::GetSlot(0), 2); b.emit(Op::GetSlot(0), 2); b.emit(Op::Dec, 2); b.emit(Op::Call(fname, 1), 2); b.emit(Op::Mul, 2); b.emit(Op::ReturnValue, 2);
let after = b.current_pos();
b.patch_jump(main_end, after);
let mut vm = VM::new(b.build());
match vm.run() {
VMResult::Ok(Value::Int(120)) => {}
other => panic!("expected 120, got {:?}", other),
}
}
#[test]
fn chunk_with_floats_roundtrips_via_json() {
let mut b = ChunkBuilder::new();
let s_idx = b.add_constant(Value::str("hello"));
b.emit(Op::LoadFloat(3.14159), 1);
b.emit(Op::LoadFloat(2.71828), 1);
b.emit(Op::Mul, 1);
b.emit(Op::LoadConst(s_idx), 1);
b.emit(Op::StringLen, 1);
let chunk = b.build();
let json = serde_json::to_string(&chunk).expect("serialize");
let restored: fusevm::Chunk = serde_json::from_str(&json).expect("deserialize");
let mut vm_a = VM::new(chunk);
let mut vm_b = VM::new(restored);
let a = vm_a.run();
let b = vm_b.run();
let (Value::Int(a_n), Value::Int(b_n)) = (
match a {
VMResult::Ok(v) => v,
_ => panic!(),
},
match b {
VMResult::Ok(v) => v,
_ => panic!(),
},
) else {
panic!("expected Int results");
};
assert_eq!(a_n, b_n);
}
#[test]
fn builtin_can_be_called_multiple_times() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(10), 1);
b.emit(Op::CallBuiltin(0, 1), 1);
b.emit(Op::LoadInt(5), 1);
b.emit(Op::CallBuiltin(0, 1), 1);
b.emit(Op::Add, 1);
let mut vm = VM::new(b.build());
vm.register_builtin(0, |vm, _argc| {
let v = vm.pop();
Value::Int(v.to_int() * 2)
});
match vm.run() {
VMResult::Ok(Value::Int(30)) => {} other => panic!("got {:?}", other),
}
}
#[test]
fn empty_range_produces_empty_array() {
let v = run(&[(Op::LoadInt(5), 1), (Op::LoadInt(3), 1), (Op::Range, 1)]);
if let Value::Array(ref a) = v {
assert!(a.is_empty());
} else {
panic!("expected Array, got {:?}", v);
}
}
#[test]
fn single_element_range() {
let v = run(&[(Op::LoadInt(5), 1), (Op::LoadInt(5), 1), (Op::Range, 1)]);
if let Value::Array(ref a) = v {
assert_eq!(a.len(), 1);
assert_eq!(a[0], Value::Int(5));
} else {
panic!()
}
}
#[test]
fn range_step_zero_yields_empty() {
let v = run(&[
(Op::LoadInt(0), 1),
(Op::LoadInt(10), 1),
(Op::LoadInt(0), 1),
(Op::RangeStep, 1),
]);
if let Value::Array(ref a) = v {
assert!(a.is_empty());
} else {
panic!()
}
}
#[test]
fn range_step_negative_descends() {
let v = run(&[
(Op::LoadInt(10), 1),
(Op::LoadInt(1), 1),
(Op::LoadInt(-2), 1),
(Op::RangeStep, 1),
]);
if let Value::Array(ref a) = v {
assert_eq!(a.len(), 5);
assert_eq!(a[0], Value::Int(10));
assert_eq!(a[4], Value::Int(2));
} else {
panic!()
}
}
#[test]
fn log_and_evaluates_both_sides() {
let b = run_bool(&[(Op::LoadTrue, 1), (Op::LoadFalse, 1), (Op::LogAnd, 1)]);
assert!(!b);
}
#[test]
fn log_or_with_falsy_strings() {
let mut bb = ChunkBuilder::new();
let e1 = bb.add_constant(Value::str(""));
let e2 = bb.add_constant(Value::str("0"));
bb.emit(Op::LoadConst(e1), 1);
bb.emit(Op::LoadConst(e2), 1);
bb.emit(Op::LogOr, 1);
let mut vm = VM::new(bb.build());
match vm.run() {
VMResult::Ok(Value::Bool(false)) => {}
other => panic!("got {:?}", other),
}
}
#[test]
fn log_not_inverts_truthiness() {
let b = run_bool(&[(Op::LoadInt(0), 1), (Op::LogNot, 1)]);
assert!(b);
let b = run_bool(&[(Op::LoadInt(42), 1), (Op::LogNot, 1)]);
assert!(!b);
}