//! Verifier Golden Test Suite //! //! This suite exercises a stable set of valid and invalid bytecode samples //! and asserts either successful verification or specific `VerifierError` //! kinds. All tests are deterministic and self-contained. use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_vm::verifier::{Verifier, VerifierError}; // --- Small helpers --------------------------------------------------------------------------- fn enc_op(op: OpCode) -> [u8; 2] { // Opcodes are encoded as u16 LE; CoreOpCode fits in u8 so we write [lo, hi=0x00] [(op as u8), 0x00] } fn func(meta: FunctionMeta) -> Vec { vec![meta] } // --- Valid Programs -------------------------------------------------------------------------- #[test] fn golden_valid_empty_function() { // Empty function is allowed by the verifier. let code = vec![]; let functions = func(FunctionMeta { code_offset: 0, code_len: 0, ..Default::default() }); let res = Verifier::verify(&code, &functions).unwrap(); assert_eq!(res[0], 0); } #[test] fn golden_valid_simple_arith_and_ret() { // push 1; push 2; add; ret let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&1u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&2u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Add)); code.extend_from_slice(&enc_op(OpCode::Ret)); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 1, ..Default::default() }); let res = Verifier::verify(&code, &functions).unwrap(); assert_eq!(res[0], 2); // max stack reaches 2 after two pushes } #[test] fn golden_valid_jump_to_end() { // Single-instr function that jumps to exactly end (allowed terminator path) let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Jmp)); code.extend_from_slice(&6u32.to_le_bytes()); let functions = func(FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }); let res = Verifier::verify(&code, &functions).unwrap(); assert_eq!(res[0], 0); } // --- Invalid Programs (one per error category) ----------------------------------------------- #[test] fn golden_err_unknown_opcode() { // Write u16 opcode that does not map to any CoreOpCode let code = vec![0xFF, 0xFF]; let functions = func(FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::UnknownOpcode { pc: 0, opcode: 0xFFFF })); } #[test] fn golden_err_truncated_opcode() { let code = vec![OpCode::PushI32 as u8]; // only 1 byte of a u16 opcode let functions = func(FunctionMeta { code_offset: 0, code_len: 1, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 })); } #[test] fn golden_err_truncated_immediate() { // PushI32 requires 4-byte immediate, provide only 1 let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.push(0xAA); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!( res, Err(VerifierError::TruncatedImmediate { pc: 0, opcode: OpCode::PushI32, need: 4, have: 1 }) ); } #[test] fn golden_err_invalid_jump_target() { let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Jmp)); code.extend_from_slice(&100u32.to_le_bytes()); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 })); } #[test] fn golden_err_jump_to_mid_instruction() { // [PushI32 4 bytes] then jmp to offset 1 (middle of first instruction) let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&42u32.to_le_bytes()); // at pc 0..6 code.extend_from_slice(&enc_op(OpCode::Jmp)); // at pc 6..8 code.extend_from_slice(&1u32.to_le_bytes()); // target 1 let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }); let res = Verifier::verify(&code, &functions); // The verifier reports error at the jmp site pc (func_start + pc==6) with target=1 assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 })); } #[test] fn golden_err_stack_underflow() { // Add requires 2 inputs; stack is empty let code = vec![OpCode::Add as u8, 0x00]; let functions = func(FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add })); } #[test] fn golden_err_stack_overflow() { // Two pushes would raise max stack to 2, but limit is set to 1 let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&1u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&2u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Ret)); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 1, max_stack_slots: 1, ..Default::default() }); let res = Verifier::verify(&code, &functions); // Overflow happens when second push increases height from 1 -> 2 over limit=1; pc is start of that push // First push consumes 6 bytes; second push opcode begins at pc=6 assert_eq!(res, Err(VerifierError::StackOverflow { pc: 6, height: 2, limit: 1 })); } #[test] fn golden_err_stack_mismatch_join() { // Branch that merges with different heights at join point // 0: PushBool true (3) // 3: JmpIfTrue 15 (6) // 9: Jmp 27 (6) // 15: PushI32 1 (6) // 21: Jmp 27 (6) // 27: Nop (2) let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::PushBool)); code.push(1); code.extend_from_slice(&enc_op(OpCode::JmpIfTrue)); code.extend_from_slice(&15u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Jmp)); code.extend_from_slice(&27u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&1u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Jmp)); code.extend_from_slice(&27u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Nop)); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!( res, Err(VerifierError::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 }) ); } #[test] fn golden_err_bad_ret_stack_height() { // Push one value but function declares 0 returns let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::PushI32)); code.extend_from_slice(&1u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Ret)); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 0, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::BadRetStackHeight { pc: 6, height: 1, expected: 0 })); } #[test] fn golden_err_function_out_of_bounds() { // Function start is beyond the available code length let code = vec![enc_op(OpCode::Nop)[0], enc_op(OpCode::Nop)[1]]; // len = 2 let functions = func(FunctionMeta { code_offset: 10, code_len: 0, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert!(matches!(res, Err(VerifierError::FunctionOutOfBounds { func_idx: 0, start: 10, end, code_len: 2 }))); } #[test] fn golden_err_trailing_bytes() { // With current decoder loop, a stray byte at the end is reported as TruncatedOpcode let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Nop)); // 2 bytes code.push(0xCC); // stray byte let functions = func(FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 2 })); } #[test] fn golden_err_unterminated_path() { // Code that falls off end without terminator (no ret/jmp to end/trap/halt) let code = vec![enc_op(OpCode::Nop)[0], enc_op(OpCode::Nop)[1]]; // single NOP, then fallthrough let functions = func(FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::UnterminatedPath { func_idx: 0, at_pc: 0 })); } #[test] fn golden_err_invalid_syscall_id() { // Syscall expects valid `prometeu_hal::syscalls::Syscall` id; use an unknown value let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Syscall)); code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF })); } #[test] fn golden_err_syscall_too_few_args() { // Choose a syscall that requires 2 arg slots and returns 0: LogWrite (0x5001) // Program: SYSCALL 0x5001; RET // No arguments are pushed before the syscall, so verifier must catch underflow at the syscall. let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Syscall)); code.extend_from_slice(&0x5001u32.to_le_bytes()); code.extend_from_slice(&enc_op(OpCode::Ret)); let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 0, ..Default::default() }); let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Syscall })); } #[test] fn golden_ok_syscall_args_and_returns() { // Case A: Syscall with 2 args, 0 returns (LogWrite): push 2; syscall; ret (ret_slots = 0) let mut code_a = Vec::new(); code_a.extend_from_slice(&enc_op(OpCode::PushI32)); code_a.extend_from_slice(&1i32.to_le_bytes()); code_a.extend_from_slice(&enc_op(OpCode::PushI32)); code_a.extend_from_slice(&2i32.to_le_bytes()); code_a.extend_from_slice(&enc_op(OpCode::Syscall)); code_a.extend_from_slice(&0x5001u32.to_le_bytes()); // LogWrite: 2 args, 0 returns code_a.extend_from_slice(&enc_op(OpCode::Ret)); let functions_a = func(FunctionMeta { code_offset: 0, code_len: code_a.len() as u32, return_slots: 0, ..Default::default() }); let res_a = Verifier::verify(&code_a, &functions_a).unwrap(); // Max stack height reaches 2 after two pushes assert!(res_a[0] >= 2); // Case B: Syscall with 0 args, 1 return (TouchGetX: 0x2101): syscall; ret (ret_slots = 1) let mut code_b = Vec::new(); code_b.extend_from_slice(&enc_op(OpCode::Syscall)); code_b.extend_from_slice(&0x2101u32.to_le_bytes()); // TouchGetX: 0 args, 1 return code_b.extend_from_slice(&enc_op(OpCode::Ret)); let functions_b = func(FunctionMeta { code_offset: 0, code_len: code_b.len() as u32, return_slots: 1, ..Default::default() }); let res_b = Verifier::verify(&code_b, &functions_b).unwrap(); // Max stack height should be at least 1 due to the return value assert!(res_b[0] >= 1); } #[test] fn golden_err_invalid_func_id() { // Encode a Call with invalid function id (beyond functions.len()) // We use two functions meta entries; first is caller, second is callee placeholder. // The call uses id=5 which is out-of-range → InvalidFuncId. let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Call)); code.extend_from_slice(&5u32.to_le_bytes()); // Caller function only. No actual callee at index 5. let functions = vec![FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidFuncId { pc: 0, id: 5 })); }