use crate::lookup_intrinsic_by_id; use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::isa::core::CoreOpCodeSpecExt as OpCodeSpecExt; use prometeu_bytecode::{DecodeError, decode_next}; use prometeu_bytecode::{FunctionLayout, compute_function_layouts}; use prometeu_hal::syscalls::Syscall; use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum VerifierError { UnknownOpcode { pc: usize, opcode: u16, }, TruncatedOpcode { pc: usize, }, TruncatedImmediate { pc: usize, opcode: OpCode, need: usize, have: usize, }, InvalidJumpTarget { pc: usize, target: usize, }, JumpToMidInstruction { pc: usize, target: usize, }, StackUnderflow { pc: usize, opcode: OpCode, }, StackOverflow { pc: usize, height: u16, limit: u16, }, StackMismatchJoin { pc: usize, target: usize, height_in: u16, height_target: u16, }, BadRetStackHeight { pc: usize, height: u16, expected: u16, }, FunctionOutOfBounds { func_idx: usize, start: usize, end: usize, code_len: usize, }, InvalidSyscallId { pc: usize, id: u32, }, InvalidIntrinsicId { pc: usize, id: u32, }, TrailingBytes { func_idx: usize, at_pc: usize, }, InvalidFuncId { pc: usize, id: u32, }, HostcallNotPatched { pc: usize, sysc_index: u32, }, /// Execution can fall through past the end of the function without a valid terminator /// (e.g., RET, JMP to end, HALT/TRAP). Verifier requires every reachable path to end /// in a terminator. UnterminatedPath { func_idx: usize, at_pc: usize, }, // --- Closure-specific errors --- /// Top of stack is not a closure value on CALL_CLOSURE NotAClosureOnCallClosure { pc: usize, }, /// CALL_CLOSURE used with a closure whose callee function is not known at verify time UnknownClosureCallee { pc: usize, }, /// User-provided arg_count for CALL_CLOSURE does not match callee signature BadClosureArgCount { pc: usize, expected: u16, got: u16, }, /// YIELD executed in an invalid context (minimal safety rule violation) InvalidYieldContext { pc: usize, height: u16, }, /// SPAWN arg_count does not match callee param_slots BadSpawnArgCount { pc: usize, expected: u16, got: u16, }, } pub struct Verifier; impl Verifier { pub fn verify(code: &[u8], functions: &[FunctionMeta]) -> Result, VerifierError> { let mut max_stacks = Vec::with_capacity(functions.len()); // Precompute function [start, end) ranges once for O(1) lookups let layouts = compute_function_layouts(functions, code.len()); for (i, func) in functions.iter().enumerate() { max_stacks.push(Self::verify_function(code, func, i, functions, &layouts)?); } Ok(max_stacks) } fn verify_function( code: &[u8], func: &FunctionMeta, func_idx: usize, all_functions: &[FunctionMeta], layouts: &[FunctionLayout], ) -> Result { let func_start = func.code_offset as usize; // Use precomputed canonical range end let func_end = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len()); if func_start > code.len() || func_end > code.len() || func_start > func_end { return Err(VerifierError::FunctionOutOfBounds { func_idx, start: func_start, end: func_end, code_len: code.len(), }); } let func_code = &code[func_start..func_end]; // Empty functions (no code bytes) are considered valid in the verifier. // They do not consume or produce values on the stack and have no internal flow. // Note: if an empty function is called at runtime and return/effects // are expected, it is the responsibility of the code generator/linker to prevent this situation. if func_code.is_empty() { return Ok(0); } // First pass: find all valid instruction boundaries let mut valid_pc = HashSet::new(); let mut pc = 0; while pc < func_code.len() { valid_pc.insert(pc); let instr = decode_next(pc, func_code).map_err(|e| match e { DecodeError::UnknownOpcode { pc: _, opcode } => { VerifierError::UnknownOpcode { pc: func_start + pc, opcode } } DecodeError::TruncatedOpcode { pc: _ } => { VerifierError::TruncatedOpcode { pc: func_start + pc } } DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => { VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have } } DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => { VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need: expected, have: actual, } } })?; pc = instr.next_pc; } if pc != func_code.len() { return Err(VerifierError::TrailingBytes { func_idx, at_pc: func_start + pc }); } // Minimal stack type lattice to validate closure semantics #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum SlotTy { /// Type information not known statically (join of different paths or from loads/calls) Unknown, /// A value that is definitely not a closure (immediates, arithmetic results) NonClosure, /// A closure value whose target function id is known (from MAKE_CLOSURE) Closure(u32), } fn join_slot(a: SlotTy, b: SlotTy) -> SlotTy { use SlotTy::*; match (a, b) { (Closure(id1), Closure(id2)) if id1 == id2 => Closure(id1), (NonClosure, NonClosure) => NonClosure, _ => Unknown, } } let mut stack_types_in: HashMap> = HashMap::new(); let mut worklist = VecDeque::new(); let mut max_stack: u16 = 0; // Start from function entry stack_types_in.insert(0, Vec::new()); worklist.push_back(0); while let Some(pc) = worklist.pop_front() { let in_types = stack_types_in.get(&pc).cloned().unwrap(); let in_height = in_types.len() as u16; let instr = decode_next(pc, func_code).unwrap(); // Guaranteed to succeed due to first pass let spec = instr.opcode.spec(); // Resolve dynamic pops/_pushes let (pops, _pushes) = match instr.opcode { OpCode::PopN => { let n = instr.imm_u32().unwrap() as u16; (n, 0) } OpCode::Call => { let func_id = instr.imm_u32().unwrap(); let callee = all_functions.get(func_id as usize).ok_or_else(|| { VerifierError::InvalidFuncId { pc: func_start + pc, id: func_id } })?; (callee.param_slots, callee.return_slots) } OpCode::MakeClosure => { // imm: fn_id (u32), capture_count (u32) let (_fn_id, capture_count_u32) = instr.imm_u32x2().unwrap(); let capture_count = capture_count_u32 as u16; (capture_count, 1) } OpCode::CallClosure => { // imm: arg_count (u32). Pops closure_ref + arg_count, _pushes callee returns. // We can't determine _pushes here without looking at TOS type; will validate below. let arg_count = instr.imm_u32().unwrap() as u16; // Temporarily set _pushes=0; we'll compute real _pushes after type checks. (arg_count + 1, 0) } OpCode::Spawn => { // imm: fn_id (u32), arg_count (u32) let (fn_id, arg_count_u32) = instr.imm_u32x2().unwrap(); let callee = all_functions.get(fn_id as usize).ok_or_else(|| { VerifierError::InvalidFuncId { pc: func_start + pc, id: fn_id } })?; let arg_count = arg_count_u32 as u16; // Enforce exact arity match for safety and determinism if callee.param_slots != arg_count { return Err(VerifierError::BadSpawnArgCount { pc: func_start + pc, expected: callee.param_slots, got: arg_count, }); } (arg_count, 0) } OpCode::Ret => (func.return_slots, 0), OpCode::Hostcall => { return Err(VerifierError::HostcallNotPatched { pc: func_start + pc, sysc_index: instr.imm_u32().unwrap(), }); } OpCode::Syscall => { let id = instr.imm_u32().unwrap(); let syscall = Syscall::from_u32(id).ok_or_else(|| { VerifierError::InvalidSyscallId { pc: func_start + pc, id } })?; (syscall.args_count() as u16, syscall.results_count() as u16) } OpCode::Intrinsic => { let id = instr.imm_u32().unwrap(); let intrinsic = lookup_intrinsic_by_id(id).ok_or_else(|| { VerifierError::InvalidIntrinsicId { pc: func_start + pc, id } })?; (intrinsic.arg_slots() as u16, intrinsic.ret_slots() as u16) } _ => (spec.pops, spec.pushes), }; if in_height < pops { return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: instr.opcode, }); } // Coroutine safety: forbid YIELD when operand stack is not empty (minimal rule) if let OpCode::Yield = instr.opcode && in_height != 0 { return Err(VerifierError::InvalidYieldContext { pc: func_start + pc, height: in_height, }); } // Compute out types vector with closure-aware rules use SlotTy::*; let mut out_types = in_types.clone(); // Pop first for _ in 0..pops { out_types.pop(); } // Special handling per opcode that affects types/_pushes let mut dynamic_pushes: Option = None; match instr.opcode { OpCode::MakeClosure => { let (fn_id, _cap) = instr.imm_u32x2().unwrap(); out_types.push(Closure(fn_id)); } OpCode::CallClosure => { // Validate TOS was a known closure and arg_count matches signature let tos = in_types.last().copied().unwrap_or(Unknown); match tos { Closure(fn_id) => { let callee = all_functions.get(fn_id as usize).ok_or_else(|| { VerifierError::InvalidFuncId { pc: func_start + pc, id: fn_id } })?; let user_argc = instr.imm_u32().unwrap() as u16; if callee.param_slots == 0 { // Hidden arg0 must exist in callee signature return Err(VerifierError::BadClosureArgCount { pc: func_start + pc, expected: 0, got: user_argc, }); } let expected_user = callee.param_slots - 1; // exclude hidden arg0 if expected_user != user_argc { return Err(VerifierError::BadClosureArgCount { pc: func_start + pc, expected: expected_user, got: user_argc, }); } // Push callee returns as Unknown types for _ in 0..callee.return_slots { out_types.push(Unknown); } dynamic_pushes = Some(callee.return_slots); } NonClosure => { return Err(VerifierError::NotAClosureOnCallClosure { pc: func_start + pc, }); } Unknown => { // We cannot determine return arity; be strict and reject return Err(VerifierError::UnknownClosureCallee { pc: func_start + pc, }); } } } // Immediates and known non-closure _pushes OpCode::PushConst | OpCode::PushI64 | OpCode::PushF64 | OpCode::PushBool | OpCode::PushI32 => { out_types.push(NonClosure); } // Dup duplicates TOS type OpCode::Dup => { let tos = in_types.last().copied().unwrap_or(Unknown); if matches!(tos, Unknown) && in_height == 0 { // Will already have underflowed on pops check for other ops; for Dup, enforce explicitly return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: OpCode::Dup, }); } out_types.push(tos); } // Swap swaps top-2 types; ensure enough stack OpCode::Swap => { if in_types.len() < 2 { return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: OpCode::Swap, }); } let len = out_types.len(); out_types.swap(len - 1, len - 2); } // Arithmetic/logic produce NonClosure but we don't strictly need it; mark Unknown conservatively // Calls and syscalls return Unknown OpCode::Call => { let func_id = instr.imm_u32().unwrap(); let callee = all_functions.get(func_id as usize).unwrap(); for _ in 0..callee.return_slots { out_types.push(Unknown); } } OpCode::Syscall => { let id = instr.imm_u32().unwrap(); let syscall = Syscall::from_u32(id).unwrap(); for _ in 0..(syscall.results_count() as u16) { out_types.push(Unknown); } } OpCode::Intrinsic => { let id = instr.imm_u32().unwrap(); let intrinsic = lookup_intrinsic_by_id(id).unwrap(); for _ in 0..(intrinsic.ret_slots() as u16) { out_types.push(Unknown); } } _ => { // Default: push Unknown for any declared _pushes if spec.pushes > 0 { for _ in 0..spec.pushes { out_types.push(Unknown); } } } } let _pushes_final = dynamic_pushes.unwrap_or(match instr.opcode { OpCode::MakeClosure => 1, OpCode::CallClosure => { // If we reached here, we handled it above and set dynamic_pushes 0 } OpCode::Syscall => spec.pushes, // already added Unknowns above OpCode::Intrinsic => spec.pushes, // already added Unknowns above OpCode::Call => spec.pushes, // already added Unknowns above _ => spec.pushes, }); let out_height = (out_types.len()) as u16; max_stack = max_stack.max(out_height); // Enforce declared max stack if provided (0 means "no explicit limit") if func.max_stack_slots != 0 && out_height > func.max_stack_slots { return Err(VerifierError::StackOverflow { pc: func_start + pc, height: out_height, limit: func.max_stack_slots, }); } if instr.opcode == OpCode::Ret && in_height != func.return_slots { return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: in_height, expected: func.return_slots, }); } // Propagate to successors if spec.is_branch { // Canonical contract: branch immediate is RELATIVE to function start. let target_rel = instr.imm_u32().unwrap() as usize; let func_end_abs = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len()); let func_len = func_end_abs - func_start; if target_rel > func_len { // Mandatory structured diagnostic for InvalidJumpTarget let pc_abs = func_start + pc; let opcode = instr.opcode; let imm_raw = instr.imm_u32().unwrap(); let func_start_abs = func_start; let func_end_abs_log = func_end_abs; let target_abs_expected = func_start_abs + target_rel; let is_boundary_target_rel = valid_pc.contains(&target_rel); eprintln!( "[VERIFIER] invalid jump: pc_abs={} opcode={:?} imm_raw={} func=F{} start={} end={} len={} target_rel={} target_abs_expected={} boundary_rel={}", pc_abs, opcode, imm_raw, func_idx, func_start_abs, func_end_abs_log, func_len, target_rel, target_abs_expected, is_boundary_target_rel ); return Err(VerifierError::InvalidJumpTarget { pc: pc_abs, target: target_abs_expected, }); } if target_rel == func_len { // jump to the end of function if out_height != func.return_slots { return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: out_height, expected: func.return_slots, }); } } else { if !valid_pc.contains(&target_rel) { return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target_rel, }); } if let Some(existing_types) = stack_types_in.get_mut(&target_rel) { let existing_height = existing_types.len() as u16; if existing_height != out_height { return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target_rel, height_in: out_height, height_target: existing_height, }); } else { // Merge types pointwise; if any slot changes, re-enqueue let mut changed = false; for i in 0..existing_types.len() { let joined = join_slot(existing_types[i], out_types[i]); if joined != existing_types[i] { existing_types[i] = joined; changed = true; } } if changed { worklist.push_back(target_rel); } } } else { stack_types_in.insert(target_rel, out_types.clone()); worklist.push_back(target_rel); } } } if !spec.is_terminator { let next_pc = instr.next_pc; let func_len = layouts.get(func_idx).map(|l| l.end - l.start).unwrap_or_else(|| 0); if next_pc < func_len { if let Some(existing_types) = stack_types_in.get_mut(&next_pc) { let existing_height = existing_types.len() as u16; if existing_height != out_height { return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + next_pc, height_in: out_height, height_target: existing_height, }); } else { let mut changed = false; for i in 0..existing_types.len() { let joined = join_slot(existing_types[i], out_types[i]); if joined != existing_types[i] { existing_types[i] = joined; changed = true; } } if changed { worklist.push_back(next_pc); } } } else { stack_types_in.insert(next_pc, out_types); worklist.push_back(next_pc); } } else if next_pc == func_len { // Reaching end of function by falling through (no terminator) is invalid. return Err(VerifierError::UnterminatedPath { func_idx, at_pc: func_start + pc, }); } } } Ok(max_stack) } } // ----------------------------------------------------------------------------- // Moved integration tests: Verifier Golden + Closures // These were previously in `crates/console/prometeu-vm/tests/` but importing // `crate::verifier` from integration tests would force the verifier to be // public. To keep the module private while preserving coverage, we embed them // here as unit tests. // ----------------------------------------------------------------------------- #[cfg(test)] mod golden_ext { use super::{Verifier, VerifierError}; use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; fn enc_op(op: OpCode) -> [u8; 2] { (op as u16).to_le_bytes() } fn func(meta: FunctionMeta) -> Vec { vec![meta] } // A minimal selection from the golden suite (full file migrated from // integration tests). Keeping names to avoid confusion. #[test] fn golden_valid_empty_function() { 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() { 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!(res[0] >= 2); } #[test] fn golden_err_unknown_opcode() { 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_immediate() { 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 })); } // --- Closures subset ------------------------------------------------------------------ #[test] fn closure_call_valid_passes() { let mut code = Vec::new(); // F0 @ 0 code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&7u32.to_le_bytes()); code.push(OpCode::MakeClosure as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // fn id code.extend_from_slice(&0u32.to_le_bytes()); // cap count code.push(OpCode::CallClosure as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // argc = 1 (excludes hidden) code.push(OpCode::PopN as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let f0_len = code.len() as u32; // F1 @ f0_len code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let f1_len = (code.len() as u32) - f0_len; let functions = vec![ FunctionMeta { code_offset: 0, code_len: f0_len, return_slots: 0, ..Default::default() }, FunctionMeta { code_offset: f0_len, code_len: f1_len, param_slots: 2, return_slots: 1, ..Default::default() }, ]; let res = Verifier::verify(&code, &functions).unwrap(); assert!(res[0] >= 2); } #[test] fn call_closure_on_non_closure_fails() { let mut code = Vec::new(); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&7u32.to_le_bytes()); code.push(OpCode::CallClosure as u8); code.push(0x00); code.extend_from_slice(&0u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 0, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert!(matches!(res, Err(VerifierError::NotAClosureOnCallClosure { .. }))); } #[test] fn closure_call_wrong_argc_fails() { // Same as valid case but argc = 0 while callee expects 1 user arg let mut code = Vec::new(); // F0 @ 0 code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&7u32.to_le_bytes()); code.push(OpCode::MakeClosure as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // fn id code.extend_from_slice(&0u32.to_le_bytes()); // cap count code.push(OpCode::CallClosure as u8); code.push(0x00); code.extend_from_slice(&0u32.to_le_bytes()); // argc = 0 (mismatch) code.push(OpCode::Ret as u8); code.push(0x00); let f0_len = code.len() as u32; // F1 @ f0_len code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let f1_len = (code.len() as u32) - f0_len; let functions = vec![ FunctionMeta { code_offset: 0, code_len: f0_len, return_slots: 0, ..Default::default() }, FunctionMeta { code_offset: f0_len, code_len: f1_len, param_slots: 2, return_slots: 1, ..Default::default() }, ]; let res = Verifier::verify(&code, &functions); assert!(matches!(res, Err(VerifierError::BadClosureArgCount { .. }))); } #[test] fn nested_closure_calls_verify() { // F0: MakeClosure(fn=1,0); CallClosure argc=0; PopN 1; Ret // F1: MakeClosure(fn=2,0); CallClosure argc=0; Ret (param=1 hidden, ret=1) // F2: PushI32 5; Ret (param=1 hidden, ret=1) let mut code = Vec::new(); // F0 @ 0 code.push(OpCode::MakeClosure as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // F1 code.extend_from_slice(&0u32.to_le_bytes()); // cap=0 code.push(OpCode::CallClosure as u8); code.push(0x00); code.extend_from_slice(&0u32.to_le_bytes()); // argc=0 code.push(OpCode::PopN as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let f0_len = code.len() as u32; // F1 @ f0_len code.push(OpCode::MakeClosure as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes()); // F2 code.extend_from_slice(&0u32.to_le_bytes()); // cap=0 code.push(OpCode::CallClosure as u8); code.push(0x00); code.extend_from_slice(&0u32.to_le_bytes()); // argc=0 code.push(OpCode::Ret as u8); code.push(0x00); let f1_len = (code.len() as u32) - f0_len; // F2 @ f0_len + f1_len code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&5u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let f2_len = (code.len() as u32) - f0_len - f1_len; let functions = vec![ FunctionMeta { code_offset: 0, code_len: f0_len, return_slots: 0, ..Default::default() }, FunctionMeta { code_offset: f0_len, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() }, FunctionMeta { code_offset: f0_len + f1_len, code_len: f2_len, param_slots: 1, return_slots: 1, ..Default::default() }, ]; let res = Verifier::verify(&code, &functions).unwrap(); assert!(res[0] >= 1); } // --- Coroutines subset ---------------------------------------------------------------- #[test] fn err_invalid_yield_with_non_empty_stack() { // Program: // 0..2: PUSH_I32 1 (imm 4 bytes) // 6..8: YIELD // 8..10: 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::Yield)); code.extend_from_slice(&enc_op(OpCode::Ret)); let functions = vec![FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 1, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidYieldContext { pc: 6, height: 1 })); } #[test] fn err_spawn_arg_mismatch() { // Caller at func 0: SPAWN fn_id=1, arg_count=1; RET // Callee at func 1: expects 2 param_slots let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Spawn)); code.extend_from_slice(&1u32.to_le_bytes()); // fn_id code.extend_from_slice(&1u32.to_le_bytes()); // arg_count (mismatch: callee expects 2) code.extend_from_slice(&enc_op(OpCode::Ret)); let caller = FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 0, ..Default::default() }; // Callee has no code here; only signature matters let callee = FunctionMeta { code_offset: code.len() as u32, code_len: 0, param_slots: 2, return_slots: 0, ..Default::default() }; let functions = vec![caller, callee]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::BadSpawnArgCount { pc: 0, expected: 2, got: 1 })); } #[test] fn err_sleep_truncated_immediate() { // Encode SLEEP but provide only 1 of 4 immediate bytes let mut code = Vec::new(); code.extend_from_slice(&enc_op(OpCode::Sleep)); code.push(0xAB); 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::TruncatedImmediate { pc: 0, opcode: OpCode::Sleep, need: 4, have: 1 }) ); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_verifier_underflow() { // OpCode::Add (2 bytes) let code = vec![OpCode::Add as u8, 0x00]; let functions = vec![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 test_verifier_dup_underflow() { let code = vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]]; let functions = vec![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::Dup })); } #[test] fn test_verifier_invalid_jmp_target() { // Jmp (2 bytes) + 100u32 (4 bytes) let mut code = vec![OpCode::Jmp as u8, 0x00]; code.extend_from_slice(&100u32.to_le_bytes()); let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 })); } #[test] fn test_verifier_jmp_to_mid_instr() { // PushI32 (2 bytes) + 42u32 (4 bytes) // Jmp 1 (middle of PushI32) let mut code = vec![OpCode::PushI32 as u8, 0x00]; code.extend_from_slice(&42u32.to_le_bytes()); code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); let functions = vec![FunctionMeta { code_offset: 0, code_len: 12, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 })); } #[test] fn test_verifier_jump_to_end_ok() { // Single-instruction function where JMP targets exactly func_len (end-exclusive) // Encoding: [JMP][u32 imm], with imm == total function length (6 bytes) let mut code = Vec::new(); code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&6u32.to_le_bytes()); let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, return_slots: 0, ..Default::default() }]; let res = Verifier::verify(&code, &functions).unwrap(); // No stack usage; max stack remains 0 assert_eq!(res[0], 0); } #[test] fn test_verifier_truncation_opcode() { let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode let functions = vec![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 test_verifier_truncation_immediate() { let mut code = vec![OpCode::PushI32 as u8, 0x00]; code.push(0x42); // Only 1 byte of 4-byte immediate let functions = vec![FunctionMeta { code_offset: 0, code_len: 3, ..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 test_verifier_stack_mismatch_join() { // Let's make it reachable: // 0: PushBool true // 3: JmpIfTrue 15 // 9: Jmp 27 // 15: PushI32 1 // 21: Jmp 27 // 27: Nop let mut code = Vec::new(); code.push(OpCode::PushBool as u8); code.push(0x00); code.push(1); // 0: PushBool (3 bytes) code.push(OpCode::JmpIfTrue as u8); code.push(0x00); code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes) code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes) code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes) code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes) code.push(OpCode::Nop as u8); code.push(0x00); // 27: Nop (2 bytes) let functions = vec![FunctionMeta { code_offset: 0, code_len: 29, ..Default::default() }]; let res = Verifier::verify(&code, &functions); // Path 0->3->9->27: height 1-1+0 = 0. // Path 0->3->15->21->27: height 1-1+1 = 1. // Mismatch at 27: 0 vs 1. assert_eq!( res, Err(VerifierError::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 }) ); } #[test] fn test_verifier_bad_ret_height() { // PushI32 1 (6 bytes) // Ret (2 bytes) let mut code = vec![OpCode::PushI32 as u8, 0x00]; code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: 8, return_slots: 0, // Expected 0, but got 1 ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::BadRetStackHeight { pc: 6, height: 1, expected: 0 })); } #[test] fn test_verifier_max_stack() { // PushI32 1 // PushI32 2 // Add // Ret let mut code = Vec::new(); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes()); code.push(OpCode::Add as u8); code.push(0x00); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: 16, return_slots: 1, ..Default::default() }]; let res = Verifier::verify(&code, &functions).unwrap(); assert_eq!(res[0], 2); } #[test] fn test_verifier_stack_overflow_limit() { // Same program as max_stack, but declare max_stack_slots = 1 → should overflow at second push let mut code = Vec::new(); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes()); code.push(OpCode::Add as u8); code.push(0x00); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: 16, return_slots: 1, max_stack_slots: 1, ..Default::default() }]; let res = Verifier::verify(&code, &functions); // Overflow occurs at pc = 6 (start of second PushI32) assert_eq!(res, Err(VerifierError::StackOverflow { pc: 6, height: 2, limit: 1 })); } #[test] fn test_verifier_invalid_syscall_id() { let mut code = Vec::new(); code.push(OpCode::Syscall as u8); code.push(0x00); code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF })); } #[test] fn test_verifier_invalid_intrinsic_id() { let mut code = Vec::new(); code.push(OpCode::Intrinsic as u8); code.push(0x00); code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::InvalidIntrinsicId { pc: 0, id: 0xDEADBEEF })); } #[test] fn test_verifier_accepts_intrinsic_stack_effects() { let mut code = Vec::new(); for value in [1i32, 2, 3, 4] { code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&value.to_le_bytes()); } code.push(OpCode::Intrinsic as u8); code.push(0x00); code.extend_from_slice(&0x1000u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 1, ..Default::default() }]; let res = Verifier::verify(&code, &functions).expect("intrinsic program must verify"); assert!(res[0] >= 4); } #[test] fn test_verifier_rejects_intrinsic_stack_underflow() { let mut code = Vec::new(); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1i32.to_le_bytes()); code.push(OpCode::Intrinsic as u8); code.push(0x00); code.extend_from_slice(&0x1000u32.to_le_bytes()); code.push(OpCode::Halt as u8); code.push(0x00); 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::StackUnderflow { pc: 6, opcode: OpCode::Intrinsic })); } #[test] fn test_verifier_rejects_unpatched_hostcall() { let mut code = Vec::new(); code.push(OpCode::Hostcall as u8); code.push(0x00); code.extend_from_slice(&3u32.to_le_bytes()); let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }]; let res = Verifier::verify(&code, &functions); assert_eq!(res, Err(VerifierError::HostcallNotPatched { pc: 0, sysc_index: 3 })); } #[test] fn test_function_without_terminator_is_rejected() { // Single NOP with no RET/JMP/TRAP/HALT at the end → fallthrough to end let mut code = Vec::new(); code.push(OpCode::Nop as u8); code.push(0x00); let functions = vec![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 test_function_with_proper_terminator_passes() { // Minimal function that returns immediately let mut code = Vec::new(); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, return_slots: 0, ..Default::default() }]; let res = Verifier::verify(&code, &functions).unwrap(); assert_eq!(res[0], 0); } #[test] fn test_verifier_ret_too_few_slots() { // Function declares 1 return slot but returns nothing // 0: Ret let mut code = Vec::new(); code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, return_slots: 1, ..Default::default() }]; let res = Verifier::verify(&code, &functions); // With too few return slots at RET, the verifier raises StackUnderflow on RET assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Ret })); } #[test] fn test_verifier_call_return_mismatch_underflow_in_caller() { // Two functions: // F0 (idx 0): Call F1; PopN 1; Ret (expects 0 total at return) // F1 (idx 1): Ret (returns 0) // Caller tries to pop 1 result that callee did not return → underflow at PopN let mut code = Vec::new(); // F0 @ 0 // Call 1 code.push(OpCode::Call as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // PopN 1 code.push(OpCode::PopN as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // Ret code.push(OpCode::Ret as u8); code.push(0x00); let f0_len = code.len() as u32; // 14 // F1 @ f0_len code.push(OpCode::Ret as u8); code.push(0x00); let functions = vec![ FunctionMeta { code_offset: 0, code_len: f0_len, return_slots: 0, ..Default::default() }, FunctionMeta { code_offset: f0_len, code_len: 2, return_slots: 0, ..Default::default() }, ]; let res = Verifier::verify(&code, &functions); // Underflow at PopN (pc = 6) assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 6, opcode: OpCode::PopN })); } #[test] fn test_verifier_call_return_mismatch_bad_ret_in_caller() { // Two functions: // F0 (idx 0): Call F1; Ret (declares return_slots = 1) // F1 (idx 1): PushI32 1; PushI32 2; Ret (returns 2) // Caller ends with 2 values but declares 1 → BadRetStackHeight at caller's Ret let mut code = Vec::new(); // F0 @ 0 // Call 1 code.push(OpCode::Call as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // Ret code.push(OpCode::Ret as u8); code.push(0x00); let f0_len = code.len() as u32; // 8 // F1 @ f0_len code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes()); code.push(OpCode::Ret as u8); code.push(0x00); let f1_len = (code.len() as u32) - f0_len; let functions = vec![ FunctionMeta { code_offset: 0, code_len: f0_len, return_slots: 1, ..Default::default() }, FunctionMeta { code_offset: f0_len, code_len: f1_len, return_slots: 2, ..Default::default() }, ]; let res = Verifier::verify(&code, &functions); // Error at caller's RET (pc = 6) assert_eq!(res, Err(VerifierError::BadRetStackHeight { pc: 6, height: 2, expected: 1 })); } }