use fusevm::{ChunkBuilder, Op, VM, VMResult, Value};
fn run(b: ChunkBuilder) -> Value {
match VM::new(b.build()).run() {
VMResult::Ok(v) => v,
other => panic!("unexpected result: {:?}", other),
}
}
fn run_err(b: ChunkBuilder) -> String {
match VM::new(b.build()).run() {
VMResult::Error(msg) => msg,
other => panic!("expected Error, got {:?}", other),
}
}
#[test]
fn call_simple_doubles_argument() {
let mut b = ChunkBuilder::new();
let name = b.add_name("dbl");
b.emit(Op::LoadInt(21), 1);
b.emit(Op::Call(name, 1), 1);
let end = b.emit(Op::Jump(0), 1);
let entry = b.current_pos();
b.add_sub_entry(name, entry);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::Mul, 1);
b.emit(Op::ReturnValue, 1);
b.patch_jump(end, b.current_pos());
assert_eq!(run(b), Value::Int(42));
}
#[test]
fn call_with_zero_args_returns_constant() {
let mut b = ChunkBuilder::new();
let name = b.add_name("seven");
b.emit(Op::Call(name, 0), 1);
let end = b.emit(Op::Jump(0), 1);
let entry = b.current_pos();
b.add_sub_entry(name, entry);
b.emit(Op::LoadInt(7), 1);
b.emit(Op::ReturnValue, 1);
b.patch_jump(end, b.current_pos());
assert_eq!(run(b), Value::Int(7));
}
#[test]
fn call_undefined_function_returns_error() {
let mut b = ChunkBuilder::new();
let name = b.add_name("nope");
b.emit(Op::Call(name, 0), 1);
let err = run_err(b);
assert!(err.contains("undefined function"));
assert!(err.contains("nope"));
}
#[test]
fn call_return_without_value_pops_args() {
let mut b = ChunkBuilder::new();
let name = b.add_name("drop");
b.emit(Op::LoadInt(100), 1);
b.emit(Op::LoadInt(200), 1);
b.emit(Op::Call(name, 2), 1);
b.emit(Op::LoadInt(5), 1);
let end = b.emit(Op::Jump(0), 1);
let entry = b.current_pos();
b.add_sub_entry(name, entry);
b.emit(Op::Return, 1);
b.patch_jump(end, b.current_pos());
assert_eq!(run(b), Value::Int(5));
}
#[test]
fn call_nested_functions() {
let mut b = ChunkBuilder::new();
let inc = b.add_name("inc");
let inc2 = b.add_name("inc2");
b.emit(Op::LoadInt(10), 1);
b.emit(Op::Call(inc2, 1), 1);
let end = b.emit(Op::Jump(0), 1);
let inc_ip = b.current_pos();
b.add_sub_entry(inc, inc_ip);
b.emit(Op::Inc, 1);
b.emit(Op::ReturnValue, 1);
let inc2_ip = b.current_pos();
b.add_sub_entry(inc2, inc2_ip);
b.emit(Op::Call(inc, 1), 1);
b.emit(Op::Call(inc, 1), 1);
b.emit(Op::ReturnValue, 1);
b.patch_jump(end, b.current_pos());
assert_eq!(run(b), Value::Int(12));
}
#[test]
fn call_recursion_factorial() {
let mut b = ChunkBuilder::new();
let fact = b.add_name("fact");
b.emit(Op::LoadInt(5), 1);
b.emit(Op::Call(fact, 1), 1);
let end = b.emit(Op::Jump(0), 1);
let entry = b.current_pos();
b.add_sub_entry(fact, entry);
b.emit(Op::Dup, 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::NumLe, 1);
let to_base = b.emit(Op::JumpIfTrue(0), 1);
b.emit(Op::Dup, 1);
b.emit(Op::Dec, 1);
b.emit(Op::Call(fact, 1), 1);
b.emit(Op::Mul, 1);
b.emit(Op::ReturnValue, 1);
let base = b.current_pos();
b.patch_jump(to_base, base);
b.emit(Op::ReturnValue, 1);
b.patch_jump(end, b.current_pos());
assert_eq!(run(b), Value::Int(120));
}
#[test]
fn return_value_at_top_level_returns_value() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(99), 1);
b.emit(Op::ReturnValue, 1);
b.emit(Op::LoadInt(0), 1);
assert_eq!(run(b), Value::Int(99));
}
#[test]
fn return_at_top_level_halts() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(5), 1);
b.emit(Op::Return, 1);
b.emit(Op::LoadInt(99), 1);
let r = VM::new(b.build()).run();
match r {
VMResult::Halted | VMResult::Ok(Value::Int(5)) => {}
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn declare_var_sets_global() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(11), 1);
b.emit(Op::DeclareVar(0), 1);
b.emit(Op::GetVar(0), 1);
assert_eq!(run(b), Value::Int(11));
}
#[test]
fn set_var_overwrites() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(1), 1);
b.emit(Op::DeclareVar(0), 1);
b.emit(Op::LoadInt(99), 1);
b.emit(Op::SetVar(0), 1);
b.emit(Op::GetVar(0), 1);
assert_eq!(run(b), Value::Int(99));
}
#[test]
fn get_undefined_var_is_undef() {
let mut b = ChunkBuilder::new();
b.emit(Op::GetVar(99), 1);
assert!(matches!(run(b), Value::Undef));
}
#[test]
fn set_var_auto_resizes() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(42), 1);
b.emit(Op::SetVar(50), 1);
b.emit(Op::GetVar(50), 1);
assert_eq!(run(b), Value::Int(42));
}
#[test]
fn vars_independent_across_indices() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(10), 1);
b.emit(Op::DeclareVar(0), 1);
b.emit(Op::LoadInt(20), 1);
b.emit(Op::DeclareVar(1), 1);
b.emit(Op::GetVar(0), 1);
b.emit(Op::GetVar(1), 1);
b.emit(Op::Add, 1);
assert_eq!(run(b), Value::Int(30));
}
#[test]
fn var_persists_across_frame_scopes() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(7), 1);
b.emit(Op::DeclareVar(0), 1);
b.emit(Op::PushFrame, 1);
b.emit(Op::PopFrame, 1);
b.emit(Op::GetVar(0), 1);
assert_eq!(run(b), Value::Int(7));
}
#[test]
fn dup_duplicates_top() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(5), 1);
b.emit(Op::Dup, 1);
b.emit(Op::Add, 1);
assert_eq!(run(b), Value::Int(10));
}
#[test]
fn dup2_duplicates_top_two() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(3), 1);
b.emit(Op::LoadInt(4), 1);
b.emit(Op::Dup2, 1);
b.emit(Op::Add, 1); b.emit(Op::Add, 1); b.emit(Op::Add, 1); assert_eq!(run(b), Value::Int(14));
}
#[test]
fn dup2_empty_stack_is_noop() {
let mut b = ChunkBuilder::new();
b.emit(Op::Dup2, 1);
b.emit(Op::LoadInt(1), 1);
assert_eq!(run(b), Value::Int(1));
}
#[test]
fn swap_swaps_top_two() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(10), 1);
b.emit(Op::LoadInt(3), 1);
b.emit(Op::Swap, 1);
b.emit(Op::Sub, 1); assert_eq!(run(b), Value::Int(-7));
}
#[test]
fn swap_empty_stack_is_noop() {
let mut b = ChunkBuilder::new();
b.emit(Op::Swap, 1);
b.emit(Op::LoadInt(2), 1);
assert_eq!(run(b), Value::Int(2));
}
#[test]
fn rot_rotates_top_three() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(1), 1);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::LoadInt(3), 1);
b.emit(Op::Rot, 1);
assert_eq!(run(b), Value::Int(1));
}
#[test]
fn rot_with_two_elements_is_noop() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(1), 1);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::Rot, 1);
assert_eq!(run(b), Value::Int(2));
}
#[test]
fn pop_removes_top() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(99), 1);
b.emit(Op::LoadInt(7), 1);
b.emit(Op::Pop, 1);
assert_eq!(run(b), Value::Int(99));
}
#[test]
fn nop_does_nothing() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(5), 1);
b.emit(Op::Nop, 1);
b.emit(Op::Nop, 1);
b.emit(Op::Nop, 1);
assert_eq!(run(b), Value::Int(5));
}
#[test]
fn push_pop_frame_pair_is_balanced() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(100), 1);
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(50), 1);
b.emit(Op::PopFrame, 1); assert_eq!(run(b), Value::Int(100));
}
#[test]
fn pop_frame_with_no_frame_is_noop() {
let mut b = ChunkBuilder::new();
b.emit(Op::PopFrame, 1);
b.emit(Op::LoadInt(3), 1);
assert_eq!(run(b), Value::Int(3));
}
#[test]
fn int_plus_float_returns_float() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(2), 1);
b.emit(Op::LoadFloat(0.5), 1);
b.emit(Op::Add, 1);
match run(b) {
Value::Float(f) => assert!((f - 2.5).abs() < 1e-9),
other => panic!("expected float, got {:?}", other),
}
}
#[test]
fn negate_int() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(15), 1);
b.emit(Op::Negate, 1);
assert_eq!(run(b), Value::Int(-15));
}
#[test]
fn negate_float() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadFloat(3.14), 1);
b.emit(Op::Negate, 1);
match run(b) {
Value::Float(f) => assert!((f + 3.14).abs() < 1e-9),
other => panic!("expected float, got {:?}", other),
}
}
#[test]
fn inc_dec_wrapping() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(i64::MAX), 1);
b.emit(Op::Inc, 1);
assert_eq!(run(b), Value::Int(i64::MIN));
}
#[test]
fn dec_wrapping_min() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(i64::MIN), 1);
b.emit(Op::Dec, 1);
assert_eq!(run(b), Value::Int(i64::MAX));
}
#[test]
fn negate_min_wraps() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(i64::MIN), 1);
b.emit(Op::Negate, 1);
assert_eq!(run(b), Value::Int(i64::MIN));
}
#[test]
fn add_wraps() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(i64::MAX), 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::Add, 1);
assert_eq!(run(b), Value::Int(i64::MIN));
}
#[test]
fn sub_wraps() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(i64::MIN), 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::Sub, 1);
assert_eq!(run(b), Value::Int(i64::MAX));
}
#[test]
fn mul_wraps() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(i64::MAX), 1);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::Mul, 1);
assert_eq!(run(b), Value::Int(-2));
}
#[test]
fn concat_two_strings() {
let mut b = ChunkBuilder::new();
let a = b.add_constant(Value::str("foo"));
let c = b.add_constant(Value::str("bar"));
b.emit(Op::LoadConst(a), 1);
b.emit(Op::LoadConst(c), 1);
b.emit(Op::Concat, 1);
match run(b) {
Value::Str(s) => assert_eq!(&*s, "foobar"),
other => panic!("expected str, got {:?}", other),
}
}
#[test]
fn concat_int_with_string_coerces() {
let mut b = ChunkBuilder::new();
let suf = b.add_constant(Value::str("ms"));
b.emit(Op::LoadInt(42), 1);
b.emit(Op::LoadConst(suf), 1);
b.emit(Op::Concat, 1);
match run(b) {
Value::Str(s) => assert_eq!(&*s, "42ms"),
other => panic!("expected str, got {:?}", other),
}
}
#[test]
fn concat_empty_with_empty() {
let mut b = ChunkBuilder::new();
let e = b.add_constant(Value::str(""));
b.emit(Op::LoadConst(e), 1);
b.emit(Op::LoadConst(e), 1);
b.emit(Op::Concat, 1);
match run(b) {
Value::Str(s) => assert_eq!(&*s, ""),
other => panic!("expected str, got {:?}", other),
}
}
#[test]
fn first_registered_sub_wins_when_duplicate_names() {
let mut b = ChunkBuilder::new();
let name = b.add_name("foo");
b.emit(Op::Call(name, 0), 1);
let end = b.emit(Op::Jump(0), 1);
let entry1 = b.current_pos();
b.add_sub_entry(name, entry1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::ReturnValue, 1);
let entry2 = b.current_pos();
b.add_sub_entry(name, entry2);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::ReturnValue, 1);
b.patch_jump(end, b.current_pos());
match run(b) {
Value::Int(1) | Value::Int(2) => {}
other => panic!("expected 1 or 2, got {:?}", other),
}
}
#[test]
fn deep_arithmetic_chain() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
for i in 1..=50 {
b.emit(Op::LoadInt(i), 1);
b.emit(Op::Add, 1);
}
assert_eq!(run(b), Value::Int(50 * 51 / 2));
}
#[test]
fn ternary_select_via_jump_if_false() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadTrue, 1);
b.emit(Op::JumpIfFalse(4), 1);
b.emit(Op::LoadInt(7), 1);
b.emit(Op::Jump(5), 1);
b.emit(Op::LoadInt(99), 1);
assert_eq!(run(b), Value::Int(7));
}
#[test]
fn ternary_select_false_branch() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadFalse, 1);
b.emit(Op::JumpIfFalse(4), 1);
b.emit(Op::LoadInt(7), 1);
b.emit(Op::Jump(5), 1);
b.emit(Op::LoadInt(99), 1);
assert_eq!(run(b), Value::Int(99));
}
#[test]
fn short_circuit_and_via_keep() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadFalse, 1);
b.emit(Op::JumpIfFalseKeep(4), 1);
b.emit(Op::Pop, 1);
b.emit(Op::LoadInt(42), 1);
assert!(matches!(run(b), Value::Bool(false)));
}
#[test]
fn short_circuit_or_via_keep() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(7), 1); b.emit(Op::JumpIfTrueKeep(4), 1);
b.emit(Op::Pop, 1);
b.emit(Op::LoadInt(99), 1);
assert_eq!(run(b), Value::Int(7));
}