From b9723cfc401ca7497707238fa98a0dac5057b55f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 19 Feb 2026 14:06:56 +0000 Subject: [PATCH] pr4.7 --- .../prometeu-vm/tests/verifier_golden.rs | 241 ++++++++++++++++++ files/TODOs.md | 48 ---- 2 files changed, 241 insertions(+), 48 deletions(-) create mode 100644 crates/console/prometeu-vm/tests/verifier_golden.rs diff --git a/crates/console/prometeu-vm/tests/verifier_golden.rs b/crates/console/prometeu-vm/tests/verifier_golden.rs new file mode 100644 index 00000000..976229c1 --- /dev/null +++ b/crates/console/prometeu-vm/tests/verifier_golden.rs @@ -0,0 +1,241 @@ +//! 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_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 })); +} diff --git a/files/TODOs.md b/files/TODOs.md index 3ac93bde..460d3434 100644 --- a/files/TODOs.md +++ b/files/TODOs.md @@ -1,51 +1,3 @@ -# PR-4.7 — Verifier Golden Test Suite - -### Briefing - -We need a stable suite of valid and invalid bytecode samples to ensure verifier correctness over time. - -### Target - -* Introduce golden tests for the verifier. - -### Work items - -* Add a set of small bytecode samples: - - * Valid programs. - * Invalid programs for each verifier error. -* Implement golden tests asserting: - - * Successful verification. - * Specific error kinds for invalid programs. - -### Acceptance checklist - -* [ ] Golden tests exist for all verifier error categories. -* [ ] Tests are deterministic. -* [ ] `cargo test` passes. - -### Tests - -* New golden tests only. - -### Junie instructions - -**You MAY:** - -* Add deterministic golden tests. - -**You MUST NOT:** - -* Modify verifier logic to fit tests without understanding the cause. -* Introduce random or timing-based tests. - -**If unclear:** - -* Ask before changing test expectations. - ---- - # PR-5.1 — Define Canonical Syscall Metadata Table ### Briefing