use crate::bytecode::codec;
use crate::{Instruction, InstructionBuilder, Program, Register};
use super::error::VerifierError;
use super::jump_target::JumpTargetPhase;
use super::loop_nesting::LoopNestingPhase;
use super::phase::{Phase, Verifier};
use super::register_type::RegisterTypePhase;
use super::stack_depth::StackDepthPhase;
use super::structural::StructuralPhase;
use super::verify;
fn bytes(instrs: &[Instruction]) -> Vec<u8> {
instrs.iter().flat_map(codec::encode).collect()
}
#[test]
fn structural_accepts_valid_stream() {
let mut b = InstructionBuilder::new();
let _ = b.emit_push(1).emit_push(2).emit_add().emit_halt();
assert!(StructuralPhase.run(&b.build().unwrap()).is_ok());
}
#[test]
fn structural_rejects_truncated_push2() {
let prog = Program::new(vec![0x12, 0x00]);
let err = StructuralPhase.run(&prog).unwrap_err();
assert_eq!(err.variant_name(), "TruncatedInstruction");
}
#[test]
fn structural_rejects_reserved_opcode() {
let err = StructuralPhase.run(&Program::new(vec![0x0D])).unwrap_err();
assert_eq!(err.variant_name(), "BadOpcode");
}
#[test]
fn jump_target_accepts_valid_label() {
let code = bytes(&[
Instruction::Target {},
Instruction::Jump1 { label: 0 },
Instruction::Halt {},
]);
assert!(JumpTargetPhase.run(&Program::new(code)).is_ok());
}
#[test]
fn jump1_to_nonexistent_label() {
let code = bytes(&[Instruction::Jump1 { label: 0 }, Instruction::Halt {}]);
let err = JumpTargetPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "UndefinedJumpTarget");
}
#[test]
fn jumpi1_to_nonexistent_label() {
let code = bytes(&[Instruction::JumpI1 { label: 0 }, Instruction::Halt {}]);
let err = JumpTargetPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "UndefinedJumpTarget");
}
#[test]
fn jump2_to_nonexistent_label() {
let code = bytes(&[Instruction::Jump2 { label: 0 }, Instruction::Halt {}]);
let err = JumpTargetPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "UndefinedJumpTarget");
}
#[test]
fn jump2_label_out_of_range_with_one_target() {
let code = bytes(&[
Instruction::Target {},
Instruction::Jump2 { label: 1 },
Instruction::Halt {},
]);
let err = JumpTargetPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "UndefinedJumpTarget");
}
#[test]
fn loop_nesting_accepts_balanced_range() {
let mut b = InstructionBuilder::new();
let _ = b
.emit_push(0)
.emit_push(3)
.emit_range()
.emit_next()
.emit_halt();
assert!(LoopNestingPhase.run(&b.build().unwrap()).is_ok());
}
#[test]
fn unmatched_range_at_eof() {
let code = bytes(&[Instruction::Range {}, Instruction::Halt {}]);
let err = LoopNestingPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "UnmatchedLoop");
}
#[test]
fn next_outside_any_loop() {
let code = bytes(&[Instruction::Next {}, Instruction::Halt {}]);
let err = LoopNestingPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "NoActiveLoop");
}
#[test]
fn lval_outside_loop() {
let code = bytes(&[Instruction::LVal { reg: Register(0) }, Instruction::Halt {}]);
let err = LoopNestingPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "NoActiveLoop");
}
#[test]
fn lidx_outside_loop() {
let code = bytes(&[Instruction::Lidx { reg: Register(0) }, Instruction::Halt {}]);
let err = LoopNestingPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "NoActiveLoop");
}
#[test]
fn nested_loops_balanced() {
let code = bytes(&[
Instruction::Range {},
Instruction::Range {},
Instruction::Next {},
Instruction::Next {},
Instruction::Halt {},
]);
assert!(LoopNestingPhase.run(&Program::new(code)).is_ok());
}
#[test]
fn nested_loops_outer_unmatched() {
let code = bytes(&[
Instruction::Range {},
Instruction::Range {},
Instruction::Next {},
Instruction::Halt {},
]);
let err = LoopNestingPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "UnmatchedLoop");
assert!(matches!(
err,
VerifierError::UnmatchedLoop {
offset: 0,
depth: 1
}
));
}
#[test]
fn reg_type_stow_then_load_ok() {
let code = bytes(&[
Instruction::Push1 { val: [7] },
Instruction::Stow { reg: Register(0) },
Instruction::Load { reg: Register(0) },
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code)).is_ok());
}
#[test]
fn reg_type_load_unset_register_is_error() {
let code = bytes(&[Instruction::Load { reg: Register(1) }, Instruction::Halt {}]);
let err = RegisterTypePhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "ReadUnsetRegister");
}
#[test]
fn reg_type_output_unset_register_is_error() {
let code = bytes(&[
Instruction::Push1 { val: [0] }, Instruction::Output { reg: Register(2) },
Instruction::Halt {},
]);
let err = RegisterTypePhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "ReadUnsetRegister");
}
#[test]
fn reg_type_drop_clears_register() {
let code = bytes(&[
Instruction::Push1 { val: [1] },
Instruction::Stow { reg: Register(0) },
Instruction::Drop { reg: Register(0) },
Instruction::Load { reg: Register(0) },
Instruction::Halt {},
]);
let err = RegisterTypePhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "ReadUnsetRegister");
}
#[test]
fn reg_type_wrong_type_for_load() {
let code = bytes(&[
Instruction::Push1 { val: [4] }, Instruction::Bqmx { reg: Register(0) },
Instruction::Load { reg: Register(0) },
Instruction::Halt {},
]);
let err = RegisterTypePhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "RegisterTypeMismatch");
}
#[test]
fn reg_type_input_satisfies_any_read() {
let code = bytes(&[
Instruction::Push1 { val: [0] }, Instruction::Input { reg: Register(0) },
Instruction::Load { reg: Register(0) },
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code)).is_ok());
}
#[test]
fn reg_type_bqmx_then_getline_ok() {
let code = bytes(&[
Instruction::Push1 { val: [4] },
Instruction::Bqmx { reg: Register(0) },
Instruction::Push1 { val: [0] },
Instruction::GetLine { reg: Register(0) },
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code)).is_ok());
}
#[test]
fn reg_type_veci_then_getline_mismatch() {
let code = bytes(&[
Instruction::VecI { reg: Register(0) },
Instruction::Push1 { val: [0] },
Instruction::GetLine { reg: Register(0) },
Instruction::Halt {},
]);
let err = RegisterTypePhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "RegisterTypeMismatch");
}
#[test]
fn reg_type_energy_wrong_sample_slot() {
let code = bytes(&[
Instruction::Push1 { val: [2] },
Instruction::Bqmx { reg: Register(0) }, Instruction::Push1 { val: [2] },
Instruction::Bsmx { reg: Register(1) }, Instruction::Energy {
model: Register(1),
sample: Register(0),
},
Instruction::Halt {},
]);
let err = RegisterTypePhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "RegisterTypeMismatch");
}
#[test]
fn reg_type_correct_energy_program() {
let code = bytes(&[
Instruction::Push1 { val: [2] },
Instruction::Bqmx { reg: Register(0) }, Instruction::Push1 { val: [2] },
Instruction::Bsmx { reg: Register(1) }, Instruction::Energy {
model: Register(0),
sample: Register(1),
},
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code)).is_ok());
}
#[test]
fn reg_type_veclen_accepts_vec_xqmx() {
let code = bytes(&[
Instruction::VecX { reg: Register(0) }, Instruction::VecLen { reg: Register(0) },
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code)).is_ok());
}
#[test]
fn reg_type_resize_accepts_model_or_sample() {
let code_model = bytes(&[
Instruction::Push1 { val: [4] },
Instruction::Bqmx { reg: Register(0) },
Instruction::Push1 { val: [8] },
Instruction::Push1 { val: [8] },
Instruction::Resize { reg: Register(0) },
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code_model)).is_ok());
let code_sample = bytes(&[
Instruction::Push1 { val: [4] },
Instruction::Bsmx { reg: Register(0) },
Instruction::Push1 { val: [8] },
Instruction::Push1 { val: [8] },
Instruction::Resize { reg: Register(0) },
Instruction::Halt {},
]);
assert!(RegisterTypePhase.run(&Program::new(code_sample)).is_ok());
}
#[test]
fn default_verifier_passes_valid_program() {
let mut b = InstructionBuilder::new();
let _ = b.emit_push(10).emit_push(32).emit_add().emit_halt();
assert!(verify(&b.build().unwrap()).is_ok());
}
#[test]
fn custom_verifier_only_jump_phase() {
let code = bytes(&[Instruction::Range {}, Instruction::Halt {}]);
let result = Verifier::new()
.with_phase(JumpTargetPhase)
.run(&Program::new(code));
assert!(
result.is_ok(),
"jump-only verifier should not catch loop errors"
);
}
#[test]
fn custom_verifier_structural_then_loop() {
let prog = Program::new(vec![0x0D]);
let err = Verifier::new()
.with_phase(StructuralPhase)
.with_phase(LoopNestingPhase)
.run(&prog)
.unwrap_err();
assert_eq!(err.variant_name(), "BadOpcode");
}
#[test]
fn stack_depth_mismatch_at_conditional_join() {
let mut b = InstructionBuilder::new();
let label = b.label();
let _ = b
.emit_push(1)
.emit_jump_if(label) .emit_push(2) .place(label)
.unwrap()
.emit_halt();
let err = StackDepthPhase.run(&b.build().unwrap()).unwrap_err();
assert_eq!(err.variant_name(), "StackDepthMismatch");
}
#[test]
fn stack_effect_underflow_on_pop_empty() {
let code = bytes(&[Instruction::Pop {}, Instruction::Halt {}]);
let err = StackDepthPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "StackUnderflow");
}
#[test]
fn stack_effect_underflow_on_binary_op() {
let code = bytes(&[Instruction::Add {}, Instruction::Halt {}]);
let err = StackDepthPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "StackUnderflow");
}
#[test]
fn stack_effect_valid_push_pop() {
let code = bytes(&[
Instruction::Push1 { val: [42] },
Instruction::Pop {},
Instruction::Halt {},
]);
assert!(StackDepthPhase.run(&Program::new(code)).is_ok());
}
#[test]
fn stack_effect_sclr_resets_depth() {
let code = bytes(&[
Instruction::Push1 { val: [1] },
Instruction::Push1 { val: [2] },
Instruction::Sclr {},
Instruction::Pop {},
Instruction::Halt {},
]);
let err = StackDepthPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "StackUnderflow");
}
#[test]
fn stack_effect_sclr_leaves_clean_stack() {
let code = bytes(&[
Instruction::Push1 { val: [1] },
Instruction::Push1 { val: [2] },
Instruction::Sclr {},
Instruction::Halt {},
]);
assert!(StackDepthPhase.run(&Program::new(code)).is_ok());
}
#[test]
fn sclr_inside_loop_body_causes_imbalance() {
let code = bytes(&[
Instruction::Push1 { val: [1] }, Instruction::Push1 { val: [0] }, Instruction::Push1 { val: [3] }, Instruction::Range {}, Instruction::Sclr {}, Instruction::Next {},
Instruction::Halt {},
]);
let err = StackDepthPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "LoopStackImbalance");
}
#[test]
fn loop_body_neutral_passes() {
let code = bytes(&[
Instruction::Push1 { val: [0] },
Instruction::Push1 { val: [3] },
Instruction::Range {},
Instruction::Next {},
Instruction::Halt {},
]);
assert!(StackDepthPhase.run(&Program::new(code)).is_ok());
}
#[test]
fn loop_body_push_causes_imbalance() {
let code = bytes(&[
Instruction::Push1 { val: [0] },
Instruction::Push1 { val: [3] },
Instruction::Range {},
Instruction::Push1 { val: [99] }, Instruction::Next {},
Instruction::Halt {},
]);
let err = StackDepthPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "LoopStackImbalance");
}
#[test]
fn loop_body_pop_causes_imbalance() {
let code = bytes(&[
Instruction::Push1 { val: [1] }, Instruction::Push1 { val: [0] }, Instruction::Push1 { val: [3] }, Instruction::Range {}, Instruction::Pop {}, Instruction::Next {}, Instruction::Halt {},
]);
let err = StackDepthPhase.run(&Program::new(code)).unwrap_err();
assert_eq!(err.variant_name(), "LoopStackImbalance");
}
#[test]
fn nested_loops_both_neutral() {
let code = bytes(&[
Instruction::Push1 { val: [0] },
Instruction::Push1 { val: [2] },
Instruction::Range {},
Instruction::Push1 { val: [0] },
Instruction::Push1 { val: [2] },
Instruction::Range {},
Instruction::Next {},
Instruction::Next {},
Instruction::Halt {},
]);
assert!(StackDepthPhase.run(&Program::new(code)).is_ok());
}
#[test]
fn stack_effect_on_all_variants_does_not_panic() {
use crate::bytecode::types::Register;
let instrs: &[Instruction] = &[
Instruction::Target {},
Instruction::Jump1 { label: 0 },
Instruction::JumpI1 { label: 0 },
Instruction::Nop {},
Instruction::Halt {},
Instruction::Sclr {},
Instruction::Add {},
Instruction::Energy {
model: Register(0),
sample: Register(1),
},
];
for instr in instrs {
let _ = instr.stack_effect();
}
}