use fusevm::{ChunkBuilder, Op, VMResult, Value, VM};
fn run(b: ChunkBuilder) -> Value {
match VM::new(b.build()).run() {
VMResult::Ok(v) => v,
VMResult::Halted => Value::Undef,
VMResult::Error(e) => panic!("unexpected VM error: {e}"),
}
}
fn i(v: Value) -> i64 {
match v {
Value::Int(n) => n,
other => panic!("expected Int, got {:?}", other),
}
}
#[test]
fn preincslot_increments_and_pushes_new_value() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(5), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::PreIncSlot(0), 1);
assert_eq!(i(run(b)), 6);
}
#[test]
fn preincslot_on_unset_slot_treats_as_zero_then_increments() {
let mut b = ChunkBuilder::new();
b.emit(Op::PreIncSlot(0), 1);
assert_eq!(i(run(b)), 1);
}
#[test]
fn preincslot_followed_by_getslot_observes_increment() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(9), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::PreIncSlot(0), 1);
b.emit(Op::Pop, 1);
b.emit(Op::GetSlot(0), 1);
assert_eq!(i(run(b)), 10);
}
#[test]
fn preincslotvoid_increments_without_pushing() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(777), 1);
b.emit(Op::PreIncSlotVoid(0), 1);
assert_eq!(i(run(b)), 777);
}
#[test]
fn preincslotvoid_updates_slot_value() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(40), 1);
b.emit(Op::SetSlot(2), 1);
b.emit(Op::PreIncSlotVoid(2), 1);
b.emit(Op::PreIncSlotVoid(2), 1);
b.emit(Op::GetSlot(2), 1);
assert_eq!(i(run(b)), 42);
}
#[test]
fn slotltintjumpiffalse_jumps_when_slot_geq_limit() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(10), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::SlotLtIntJumpIfFalse(0, 5, 5), 1);
b.emit(Op::LoadInt(99), 1);
b.emit(Op::LoadInt(88), 1);
b.emit(Op::LoadInt(77), 1);
assert_eq!(i(run(b)), 77);
}
#[test]
fn slotltintjumpiffalse_falls_through_when_slot_lt_limit() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(2), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::SlotLtIntJumpIfFalse(0, 5, 99), 1);
b.emit(Op::LoadInt(7), 1);
assert_eq!(i(run(b)), 7);
}
#[test]
fn slotltintjumpiffalse_unset_slot_is_zero_so_falls_through() {
let mut b = ChunkBuilder::new();
b.emit(Op::SlotLtIntJumpIfFalse(0, 1, 99), 1);
b.emit(Op::LoadInt(42), 1);
assert_eq!(i(run(b)), 42);
}
#[test]
fn slotincltintjumpback_does_finite_loop() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
let loop_start = b.emit(Op::Nop, 1);
b.emit(Op::SlotIncLtIntJumpBack(0, 3, loop_start), 1);
b.emit(Op::GetSlot(0), 1);
assert_eq!(i(run(b)), 3);
}
#[test]
fn slotincltintjumpback_exits_when_reaching_limit() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(2), 1);
b.emit(Op::SetSlot(0), 1);
let after = b.emit(Op::Nop, 1);
b.emit(Op::SlotIncLtIntJumpBack(0, 3, after), 1);
b.emit(Op::GetSlot(0), 1);
assert_eq!(i(run(b)), 3);
}
#[test]
fn accumsumloop_sums_range_from_i_to_limit_minus_one() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(1), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::AccumSumLoop(0, 1, 5), 1);
b.emit(Op::GetSlot(0), 1);
assert_eq!(i(run(b)), 10);
}
#[test]
fn accumsumloop_i_geq_limit_does_nothing() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(100), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(5), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::AccumSumLoop(0, 1, 5), 1);
b.emit(Op::GetSlot(0), 1);
assert_eq!(i(run(b)), 100);
}
#[test]
fn addassignslotvoid_does_a_plus_equals_b_in_place() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(10), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(3), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::AddAssignSlotVoid(0, 1), 1);
b.emit(Op::GetSlot(0), 1);
assert_eq!(i(run(b)), 13);
}
#[test]
fn addassignslotvoid_does_not_push_to_stack() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(5), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(7), 1);
b.emit(Op::SetSlot(1), 1);
b.emit(Op::LoadInt(999), 1); b.emit(Op::AddAssignSlotVoid(0, 1), 1);
assert_eq!(i(run(b)), 999);
}
#[test]
fn concatconstloop_appends_constant_n_times() {
let mut b = ChunkBuilder::new();
let c = b.add_constant(Value::str("ab"));
b.emit(Op::LoadConst(c), 1);
b.emit(Op::SetSlot(0), 1); let initial = b.add_constant(Value::str(""));
b.emit(Op::LoadConst(initial), 1);
b.emit(Op::SetSlot(0), 1); b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(1), 1); b.emit(Op::ConcatConstLoop(c, 0, 1, 3), 1);
b.emit(Op::GetSlot(0), 1);
match run(b) {
Value::Str(s) => assert_eq!(s.as_str(), "ababab"),
other => panic!("expected Str, got {:?}", other),
}
}
#[test]
fn concatconstloop_no_iterations_keeps_initial_string() {
let mut b = ChunkBuilder::new();
let init = b.add_constant(Value::str("start"));
b.emit(Op::LoadConst(init), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt(5), 1);
b.emit(Op::SetSlot(1), 1); let c = b.add_constant(Value::str("X"));
b.emit(Op::ConcatConstLoop(c, 0, 1, 5), 1);
b.emit(Op::GetSlot(0), 1);
match run(b) {
Value::Str(s) => assert_eq!(s.as_str(), "start"),
other => panic!("expected Str, got {:?}", other),
}
}
#[test]
fn pushintrangeloop_extends_array_with_range_values() {
let mut b = ChunkBuilder::new();
b.emit(Op::DeclareArray(0), 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1); b.emit(Op::PushIntRangeLoop(0, 0, 4), 1);
b.emit(Op::ArrayLen(0), 1);
assert_eq!(i(run(b)), 4);
}
#[test]
fn pushintrangeloop_with_nonempty_initial_appends() {
let mut b = ChunkBuilder::new();
b.emit(Op::DeclareArray(0), 1);
b.emit(Op::LoadInt(99), 1);
b.emit(Op::ArrayPush(0), 1);
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::PushIntRangeLoop(0, 0, 3), 1);
b.emit(Op::ArrayLen(0), 1);
assert_eq!(i(run(b)), 4);
}
#[test]
fn pushframe_popframe_discards_intermediate_stack_values() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(7), 1);
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(99), 1);
b.emit(Op::LoadInt(99), 1);
b.emit(Op::PopFrame, 1);
assert_eq!(i(run(b)), 7);
}
#[test]
fn popframe_without_pushframe_does_not_panic() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(5), 1);
b.emit(Op::PopFrame, 1);
let _ = run(b);
}
#[test]
fn nested_push_pop_frames() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(1), 1);
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(2), 1);
b.emit(Op::PushFrame, 1);
b.emit(Op::LoadInt(3), 1);
b.emit(Op::PopFrame, 1); b.emit(Op::PopFrame, 1); assert_eq!(i(run(b)), 1);
}
#[test]
fn mapblock_is_currently_a_noop_in_vm() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(42), 1);
b.emit(Op::MapBlock(0), 1);
assert_eq!(i(run(b)), 42);
}
#[test]
fn grepblock_is_currently_a_noop_in_vm() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(7), 1);
b.emit(Op::GrepBlock(0), 1);
assert_eq!(i(run(b)), 7);
}
#[test]
fn sortblock_is_currently_a_noop_in_vm() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(9), 1);
b.emit(Op::SortBlock(0), 1);
assert_eq!(i(run(b)), 9);
}
#[test]
fn sortdefault_is_currently_a_noop_in_vm() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(11), 1);
b.emit(Op::SortDefault, 1);
assert_eq!(i(run(b)), 11);
}
#[test]
fn foreachblock_is_currently_a_noop_in_vm() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(33), 1);
b.emit(Op::ForEachBlock(0), 1);
assert_eq!(i(run(b)), 33);
}
#[test]
fn callbuiltin_invokes_registered_handler_and_pushes_result() {
fn h(_vm: &mut VM, argc: u8) -> Value {
Value::Int(argc as i64 * 2)
}
let mut b = ChunkBuilder::new();
b.emit(Op::CallBuiltin(7, 4), 1);
let mut vm = VM::new(b.build());
vm.register_builtin(7, h);
match vm.run() {
VMResult::Ok(Value::Int(8)) => {}
other => panic!("expected Int(8), got {:?}", other),
}
}
#[test]
fn callbuiltin_unregistered_does_not_panic() {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(123), 1);
b.emit(Op::CallBuiltin(254, 0), 1);
assert_eq!(i(run(b)), 123);
}
#[test]
fn callbuiltin_handler_can_pop_arguments_from_stack() {
fn sum(vm: &mut VM, argc: u8) -> Value {
let mut s = 0;
for _ in 0..argc {
s += vm.pop().to_int();
}
Value::Int(s)
}
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(10), 1);
b.emit(Op::LoadInt(20), 1);
b.emit(Op::LoadInt(30), 1);
b.emit(Op::CallBuiltin(5, 3), 1);
let mut vm = VM::new(b.build());
vm.register_builtin(5, sum);
match vm.run() {
VMResult::Ok(Value::Int(60)) => {}
other => panic!("expected Int(60), got {:?}", other),
}
}