bQUARKz c65362c186
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
dev/perf-runtime-telemetry-hot-path (#13)
Reviewed-on: #13
Co-authored-by: bQUARKz <bquarkz@gmail.com>
Co-committed-by: bQUARKz <bquarkz@gmail.com>
2026-04-10 08:32:00 +00:00

1396 lines
51 KiB
Rust

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<Vec<u16>, 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<u16, VerifierError> {
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<usize, Vec<SlotTy>> = 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<u16> = 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<FunctionMeta> {
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 }));
}
}