use crate::call_frame::CallFrame; use crate::verifier::Verifier; use crate::vm_init_error::VmInitError; use crate::{HostContext, NativeInterface}; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::ProgramImage; use prometeu_bytecode::Value; use crate::roots::{RootVisitor, visit_value_for_roots}; use crate::heap::{Heap, CoroutineState}; use crate::object::ObjectKind; use crate::scheduler::Scheduler; use prometeu_bytecode::{ TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, }; use prometeu_bytecode::HeapRef; use prometeu_hal::syscalls::caps::ALL; use prometeu_hal::vm_fault::VmFault; /// Reason why the Virtual Machine stopped execution during a specific run. /// This allows the system to decide if it should continue execution in the next tick /// or if the frame is finalized. #[derive(Debug, Clone, PartialEq, Eq)] pub enum LogicalFrameEndingReason { /// Execution reached a `FRAME_SYNC` instruction, marking the end of the logical frame. FrameSync, /// The cycle budget for the current host tick was exhausted before reaching `FRAME_SYNC`. BudgetExhausted, /// A `HALT` instruction was executed, terminating the program. Halted, /// The Program Counter (PC) reached the end of the available bytecode. EndOfRom, /// Execution hit a registered breakpoint. Breakpoint, /// A runtime trap occurred (e.g., out-of-bounds access). Trap(TrapInfo), /// A fatal error occurred that cannot be recovered (e.g., stack underflow). Panic(String), } pub(crate) enum OpError { Trap(u32, String), Panic(String), } impl From for LogicalFrameEndingReason { fn from(info: TrapInfo) -> Self { LogicalFrameEndingReason::Trap(info) } } /// A report detailing the results of an execution slice (run_budget). #[derive(Debug, Clone, PartialEq, Eq)] pub struct BudgetReport { /// Total virtual cycles consumed during this run. pub cycles_used: u64, /// Number of VM instructions executed. pub steps_executed: u32, /// The reason why this execution slice ended. pub reason: LogicalFrameEndingReason, } pub struct VirtualMachine { /// Program Counter (PC): The absolute byte offset in ROM for the next instruction. pc: usize, /// Operand Stack: The primary workspace for all mathematical and logical operations. operand_stack: Vec, /// Call Stack: Manages function call context (return addresses, frame limits). call_stack: Vec, /// Global Variable Store: Variables that persist for the lifetime of the program. globals: Vec, /// The loaded executable (Bytecode + Constant Pool), that is the ROM translated. program: ProgramImage, /// Heap Memory: Dynamic allocation pool. heap: Heap, /// Total virtual cycles consumed since the VM started. cycles: u64, /// Stop flag: true if a `HALT` opcode was encountered. halted: bool, /// Set of ROM addresses used for software breakpoints in the debugger. breakpoints: std::collections::HashSet, /// GC: number of newly allocated live objects threshold to trigger a collection at safepoint. /// The GC only runs at safepoints (e.g., FRAME_SYNC). 0 disables automatic GC. gc_alloc_threshold: usize, /// GC: snapshot of live objects count after the last collection (or VM init). last_gc_live_count: usize, /// Capability flags granted to the currently running program/cart. /// Syscalls are capability-gated using `prometeu_hal::syscalls::SyscallMeta::caps`. capabilities: prometeu_hal::syscalls::CapFlags, /// Cooperative scheduler: set to true when `YIELD` opcode is executed. /// The runtime/scheduler should only act on this at safepoints (FRAME_SYNC). yield_requested: bool, /// Absolute wake tick requested by the currently running coroutine (when it executes `SLEEP`). /// /// Canonical rule (authoritative): /// - `SLEEP N` suspends the coroutine for exactly N full scheduler ticks AFTER the current /// `FRAME_SYNC` completes. If `SLEEP` is executed during tick `T`, the coroutine must resume /// in the frame whose end-of-frame tick will be `T + N + 1`. /// - Implementation detail: we compute `wake_tick = current_tick + duration + 1` at the time /// `SLEEP` executes. The scheduler wakes sleeping coroutines when `current_tick >= wake_tick`. /// /// This definition is deterministic and eliminates off-by-one ambiguity. sleep_requested_until: Option, /// Logical tick counter advanced at each FRAME_SYNC boundary. current_tick: u64, /// Cooperative scheduler instance managing ready/sleeping queues. scheduler: Scheduler, /// Handle to the currently running coroutine (owns the active VM context). current_coro: Option, } impl Default for VirtualMachine { fn default() -> Self { Self::new(vec![], vec![]) } } impl VirtualMachine { /// Returns the current program counter. pub fn pc(&self) -> usize { self.pc } /// Returns true if there are no active call frames. pub fn call_stack_is_empty(&self) -> bool { self.call_stack.is_empty() } /// Returns up to `n` values from the top of the operand stack (top-first order). pub fn operand_stack_top(&self, n: usize) -> Vec { let len = self.operand_stack.len(); let start = len.saturating_sub(n); self.operand_stack[start..].iter().rev().cloned().collect() } /// Returns true if the VM has executed a HALT and is not currently running. pub fn is_halted(&self) -> bool { self.halted } /// Adds a software breakpoint at the given PC. pub fn insert_breakpoint(&mut self, pc: usize) { let _ = self.breakpoints.insert(pc); } /// Removes a software breakpoint at the given PC, if present. pub fn remove_breakpoint(&mut self, pc: usize) { let _ = self.breakpoints.remove(&pc); } /// Returns the list of currently configured breakpoints. pub fn breakpoints_list(&self) -> Vec { self.breakpoints.iter().cloned().collect() } // Test-only helpers for internal unit tests within this crate. #[cfg(test)] pub(crate) fn push_operand_for_test(&mut self, v: Value) { self.operand_stack.push(v); } /// Creates a new VM instance with the provided bytecode and constants. pub fn new(rom: Vec, constant_pool: Vec) -> Self { Self { pc: 0, operand_stack: Vec::new(), call_stack: Vec::new(), globals: Vec::new(), program: ProgramImage::new( rom, constant_pool, vec![], None, std::collections::HashMap::new(), ), heap: Heap::new(), cycles: 0, halted: false, breakpoints: std::collections::HashSet::new(), gc_alloc_threshold: 1024, // conservative default; tests may override last_gc_live_count: 0, capabilities: ALL, yield_requested: false, sleep_requested_until: None, current_tick: 0, scheduler: Scheduler::new(), current_coro: None, } } /// Resets the VM state and loads a new program. /// This is typically called by the Firmware when starting a new App/Cartridge. pub fn initialize( &mut self, program_bytes: Vec, entrypoint: &str, ) -> Result<(), VmInitError> { // Fail fast: reset state upfront. If we return early with an error, // the VM is left in a "halted and empty" state. self.program = ProgramImage::default(); self.pc = 0; self.operand_stack.clear(); self.call_stack.clear(); self.globals.clear(); self.heap = Heap::new(); self.cycles = 0; self.halted = true; // execution is impossible until a successful load self.last_gc_live_count = 0; self.current_tick = 0; self.sleep_requested_until = None; self.scheduler = Scheduler::new(); self.current_coro = None; // Preserve capabilities across loads; firmware may set them per cart. // Only recognized format is loadable: PBS v0 industrial format let program = if program_bytes.starts_with(b"PBS\0") { match prometeu_bytecode::BytecodeLoader::load(&program_bytes) { Ok(module) => { // Run verifier on the module let max_stacks = Verifier::verify(&module.code, &module.functions) .map_err(|e| VmInitError::VerificationFailed(format!("{:?}", e)))?; let mut program = ProgramImage::from(module); let mut functions = program.functions.as_ref().to_vec(); for (func, max_stack) in functions.iter_mut().zip(max_stacks) { func.max_stack_slots = max_stack; } program.functions = std::sync::Arc::from(functions); program } Err(prometeu_bytecode::LoadError::InvalidVersion) => { return Err(VmInitError::UnsupportedFormat); } Err(e) => { return Err(VmInitError::ImageLoadFailed(e)); } } } else { return Err(VmInitError::InvalidFormat); }; // Resolve the entrypoint: empty (defaults to func 0), numeric func_idx, or symbol name. let pc = if entrypoint.is_empty() { program.functions.first().map(|f| f.code_offset as usize).unwrap_or(0) } else if let Ok(func_idx) = entrypoint.parse::() { program .functions .get(func_idx) .map(|f| f.code_offset as usize) .ok_or(VmInitError::EntrypointNotFound)? } else { // Try to resolve as a symbol name from the exports map if let Some(&func_idx) = program.exports.get(entrypoint) { program .functions .get(func_idx as usize) .map(|f| f.code_offset as usize) .ok_or(VmInitError::EntrypointNotFound)? } else { return Err(VmInitError::EntrypointNotFound); } }; // Finalize initialization by applying the new program and PC. self.program = program; self.pc = pc; self.halted = false; // Successfully loaded, execution is now possible Ok(()) } /// Sets the capability flags for the current program. pub fn set_capabilities(&mut self, caps: prometeu_hal::syscalls::CapFlags) { self.capabilities = caps; } /// Prepares the VM to execute a specific entrypoint by setting the PC and /// pushing an initial call frame. pub fn prepare_call(&mut self, entrypoint: &str) { let func_idx = if let Ok(idx) = entrypoint.parse::() { idx } else { // Try to resolve as a symbol name self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) // Default to 0 if not found }; let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default(); let addr = callee.code_offset as usize; self.pc = addr; self.halted = false; // Pushing a sentinel frame so RET works at the top level. // The return address is set to the end of ROM, which will naturally // cause the VM to stop after returning from the entrypoint. self.operand_stack.clear(); self.call_stack.clear(); // Entrypoint also needs locals allocated. // For the sentinel frame, stack_base is always 0. if let Some(func) = self.program.functions.get(func_idx) { let total_slots = func.param_slots as u32 + func.local_slots as u32; for _ in 0..total_slots { self.operand_stack.push(Value::Null); } } self.call_stack.push(CallFrame { return_pc: self.program.rom.len() as u32, stack_base: 0, func_idx, }); // Initialize the main coroutine object. // IMPORTANT INVARIANT: // - The RUNNING coroutine's authoritative execution state lives in the VM fields // (pc, operand_stack, call_stack). // - The heap-side coroutine object is authoritative ONLY when the coroutine is suspended // (Ready/Sleeping/Finished). While running, its `stack`/`frames` should be empty. // // Therefore we do NOT clone the VM stacks into the heap here. We create the main // coroutine object with empty stack/frames and mark it as Running, and the VM already // holds the live execution context initialized above. let main_href = self.heap.allocate_coroutine( self.pc, CoroutineState::Running, 0, Vec::new(), Vec::new(), ); self.current_coro = Some(main_href); self.scheduler.set_current(self.current_coro); } /// Executes the VM for a limited number of cycles (budget). /// /// This is the heart of the deterministic execution model. Instead of running /// indefinitely, the VM runs until it consumes its allocated budget or reaches /// a synchronization point (`FRAME_SYNC`). /// /// # Arguments /// * `budget` - Maximum number of cycles allowed for this execution slice. /// * `native` - Interface for handling syscalls (Firmware/OS). /// * `hw` - Access to virtual hardware peripherals. pub fn run_budget( &mut self, budget: u64, native: &mut dyn NativeInterface, ctx: &mut HostContext, ) -> Result { let start_cycles = self.cycles; let mut steps_executed = 0; let mut ending_reason: Option = None; while (self.cycles - start_cycles) < budget && !self.halted && self.pc < self.program.rom.len() { // Debugger support: stop before executing an instruction if there's a breakpoint. // Note: we skip the check for the very first step of a slice to avoid // getting stuck on the same breakpoint repeatedly. if steps_executed > 0 && self.breakpoints.contains(&self.pc) { ending_reason = Some(LogicalFrameEndingReason::Breakpoint); break; } let pc_before = self.pc; let cycles_before = self.cycles; // Execute a single step (Fetch-Decode-Execute) if let Err(reason) = self.step(native, ctx) { ending_reason = Some(reason); break; } steps_executed += 1; // Integrity check: ensure real progress is being made to avoid infinite loops // caused by zero-cycle instructions or stuck PC. if self.pc == pc_before && self.cycles == cycles_before && !self.halted { ending_reason = Some(LogicalFrameEndingReason::Panic(format!( "VM stuck at PC 0x{:08X}", self.pc ))); break; } } // Determine why we stopped if no explicit reason (FrameSync/Breakpoint) was set. if ending_reason.is_none() { if self.halted { ending_reason = Some(LogicalFrameEndingReason::Halted); } else if self.pc >= self.program.rom.len() { ending_reason = Some(LogicalFrameEndingReason::EndOfRom); } else { ending_reason = Some(LogicalFrameEndingReason::BudgetExhausted); } } Ok(BudgetReport { cycles_used: self.cycles - start_cycles, steps_executed, reason: ending_reason.unwrap(), }) } /// Harness: run exactly `frames` logical frames deterministically. /// /// This repeatedly calls `run_budget` with the provided `budget_per_slice` until /// a full logical frame is completed (i.e., a `FrameSync` is observed). If a /// terminal condition is reached earlier (Halt/EndOfRom/Panic/Trap/Breakpoint), /// the function returns early with all collected slice reports so far. pub fn run_frames( &mut self, frames: u64, budget_per_slice: u64, native: &mut dyn NativeInterface, ctx: &mut HostContext, ) -> Result, String> { assert!(budget_per_slice > 0, "budget_per_slice must be > 0"); let mut out = Vec::new(); let mut frames_done = 0u64; while frames_done < frames { let rep = self.run_budget(budget_per_slice, native, ctx)?; let terminal = matches!( rep.reason, LogicalFrameEndingReason::Halted | LogicalFrameEndingReason::EndOfRom | LogicalFrameEndingReason::Panic(_) | LogicalFrameEndingReason::Trap(_) | LogicalFrameEndingReason::Breakpoint ); let is_frame_end = matches!(rep.reason, LogicalFrameEndingReason::FrameSync); out.push(rep); if terminal { break; } if is_frame_end { frames_done += 1; } } Ok(out) } /// Harness: alias for `run_frames(frames, ...)`. pub fn run_ticks( &mut self, ticks: u64, budget_per_slice: u64, native: &mut dyn NativeInterface, ctx: &mut HostContext, ) -> Result, String> { self.run_frames(ticks, budget_per_slice, native, ctx) } /// Harness: run until HALT/EndOfRom/Panic/Trap/Breakpoint deterministically. /// /// Repeatedly invokes `run_budget` with a fixed `budget_per_slice`, collecting /// each slice's report until a terminal condition is reached. pub fn run_until_halt( &mut self, budget_per_slice: u64, native: &mut dyn NativeInterface, ctx: &mut HostContext, ) -> Result, String> { assert!(budget_per_slice > 0, "budget_per_slice must be > 0"); let mut out = Vec::new(); loop { let rep = self.run_budget(budget_per_slice, native, ctx)?; let terminal = matches!( rep.reason, LogicalFrameEndingReason::Halted | LogicalFrameEndingReason::EndOfRom | LogicalFrameEndingReason::Panic(_) | LogicalFrameEndingReason::Trap(_) | LogicalFrameEndingReason::Breakpoint ); out.push(rep); if terminal { break; } } Ok(out) } /// Executes a single instruction at the current Program Counter (PC). /// /// This follows the classic CPU cycle: /// 1. Fetch: Read the opcode from memory. /// 2. Decode: Identify what operation to perform. /// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals. pub fn step( &mut self, native: &mut dyn NativeInterface, ctx: &mut HostContext, ) -> Result<(), LogicalFrameEndingReason> { // If there is no currently running coroutine (e.g., all are sleeping), // we cannot execute any instruction this frame. End the frame immediately // with a safepoint to advance tick and potentially wake sleepers. if self.current_coro.is_none() { self.cycles += OpCode::FrameSync.cycles(); self.handle_safepoint(); return Err(LogicalFrameEndingReason::FrameSync); } if self.halted || self.pc >= self.program.rom.len() { return Ok(()); } let start_pc = self.pc; // Fetch & Decode let instr = prometeu_bytecode::decode_next(self.pc, &self.program.rom) .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let opcode = instr.opcode; self.pc = instr.next_pc; // Execute match opcode { OpCode::Nop => {} OpCode::Halt => { self.halted = true; } OpCode::Jmp => { let target = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let func_start = self .call_stack .last() .map(|f| self.program.functions[f.func_idx].code_offset as usize) .unwrap_or(0); self.pc = func_start + target; } OpCode::JmpIfFalse => { let target = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; match val { Value::Boolean(false) => { let func_start = self .call_stack .last() .map(|f| self.program.functions[f.func_idx].code_offset as usize) .unwrap_or(0); self.pc = func_start + target; } Value::Boolean(true) => {} _ => { return Err(self.trap( TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_FALSE, got {:?}", val), start_pc as u32, )); } } } OpCode::JmpIfTrue => { let target = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; match val { Value::Boolean(true) => { let func_start = self .call_stack .last() .map(|f| self.program.functions[f.func_idx].code_offset as usize) .unwrap_or(0); self.pc = func_start + target; } Value::Boolean(false) => {} _ => { return Err(self.trap( TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_TRUE, got {:?}", val), start_pc as u32, )); } } } OpCode::Trap => { // Manual breakpoint instruction: consume cycles and signal a breakpoint self.cycles += OpCode::Trap.cycles(); return Err(LogicalFrameEndingReason::Breakpoint); } OpCode::Spawn => { // Operands: (fn_id, arg_count) let (fn_id_u32, arg_count_u32) = instr .imm_u32x2() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let fn_id = fn_id_u32 as usize; let arg_count = arg_count_u32 as usize; let callee = self.program.functions.get(fn_id).ok_or_else(|| { self.trap( TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {} in SPAWN", fn_id), start_pc as u32, ) })?; let param_slots: u16 = callee.param_slots; let local_slots: u16 = callee.local_slots; let entry_pc = callee.code_offset as usize; if arg_count as u16 != param_slots { return Err(self.trap( TRAP_TYPE, opcode as u16, format!( "SPAWN arg_count mismatch for func {}: expected {}, got {}", fn_id, param_slots, arg_count ), start_pc as u32, )); } if self.operand_stack.len() < arg_count { return Err(LogicalFrameEndingReason::Panic(format!( "Stack underflow during SPAWN to func {}: expected at least {} arguments, got {}", fn_id, arg_count, self.operand_stack.len() ))); } // Pop args top-first, then reverse to logical order arg1..argN let mut args: Vec = Vec::with_capacity(arg_count); for _ in 0..arg_count { args.push(self.pop().map_err(LogicalFrameEndingReason::Panic)?); } args.reverse(); // Build operand stack for the new coroutine: params followed by zeroed locals let mut new_stack: Vec = Vec::with_capacity((param_slots + local_slots) as usize); // Place user args as parameters for v in args { new_stack.push(v); } // Zero-init locals for _ in 0..local_slots { new_stack.push(Value::Null); } // Initial frame for the coroutine (sentinel-like return to end-of-rom) let frames = vec![CallFrame { return_pc: self.program.rom.len() as u32, stack_base: 0, func_idx: fn_id }]; let href = self.heap.allocate_coroutine( entry_pc, CoroutineState::Ready, 0, new_stack, frames, ); self.scheduler.enqueue_ready(href); } OpCode::Yield => { // Cooperative yield: record intent; actual switching only at FRAME_SYNC. self.yield_requested = true; // Do not end the slice here; we continue executing until a safepoint. } OpCode::Sleep => { // Immediate is duration in ticks. // // Canonical semantics: // SLEEP N => suspend for exactly N full scheduler ticks AFTER the current // FRAME_SYNC completes. If executed at tick T, resume in the frame whose // end-of-frame tick will be T + N + 1. // // Implementation rule: // wake_tick = current_tick + duration + 1 let duration = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as u64; let wake = self .current_tick .saturating_add(duration) .saturating_add(1); self.sleep_requested_until = Some(wake); // End the logical frame right after the instruction completes // to ensure no further instructions run until at least next tick. self.cycles += OpCode::FrameSync.cycles(); self.handle_safepoint(); return Err(LogicalFrameEndingReason::FrameSync); } OpCode::MakeClosure => { // Immediate carries (fn_id, capture_count) let (fn_id, cap_count) = instr .imm_u32x2() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; // Pop cap_count values from the operand stack, top-first. let mut temp: Vec = Vec::with_capacity(cap_count as usize); for _ in 0..cap_count { let v = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; temp.push(v); } // Preserve order so that env[0] corresponds to captured_1 (the bottom-most // among the popped values): reverse the temp vector. temp.reverse(); // Allocate closure on heap and push its reference. let href = self.heap.alloc_closure(fn_id, &temp); self.push(Value::HeapRef(href)); } OpCode::CallClosure => { // Operand carries the number of user-supplied arguments (arg1..argN). let user_arg_count = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; // Pop the closure reference from the stack (top of stack). let clos_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let href = match clos_val { Value::HeapRef(h) => h, other => { return Err(self.trap( TRAP_TYPE, opcode as u16, format!( "CALL_CLOSURE expects a closure handle at TOS, got {:?}", other ), start_pc as u32, )) } }; // Validate that the heap object is indeed a Closure. let header = self.heap.header(href).ok_or_else(|| { self.trap( TRAP_OOB, opcode as u16, format!("Invalid heap handle in CALL_CLOSURE: {:?}", href), start_pc as u32, ) })?; if header.kind != ObjectKind::Closure { return Err(self.trap( TRAP_TYPE, opcode as u16, format!( "CALL_CLOSURE on non-closure object kind {:?}", header.kind ), start_pc as u32, )); } // Pop user arguments from the operand stack (top-first), then fix order. let mut user_args: Vec = Vec::with_capacity(user_arg_count); for _ in 0..user_arg_count { user_args.push(self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?); } user_args.reverse(); // Now in logical order: arg1..argN // Resolve target function id from the closure payload. let fn_id = self.heap.closure_fn_id(href).ok_or_else(|| { LogicalFrameEndingReason::Panic( "Internal error: malformed closure object (missing fn_id)".into(), ) })? as usize; let callee = self.program.functions.get(fn_id).ok_or_else(|| { self.trap( TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {} from closure", fn_id), start_pc as u32, ) })?; // Copy required fields to drop the immutable borrow before mutating self let callee_param_slots = callee.param_slots as usize; let callee_local_slots = callee.local_slots as usize; let callee_code_offset = callee.code_offset as usize; // Validate arity: param_slots must equal hidden arg0 + user_arg_count. let expected_params = 1usize + user_arg_count; if callee_param_slots != expected_params { return Err(self.trap( TRAP_TYPE, opcode as u16, format!( "CALL_CLOSURE arg_count mismatch: function expects {} total params (including hidden arg0), got hidden+{}", callee_param_slots, expected_params ), start_pc as u32, )); } // Prepare the operand stack to match the direct CALL convention: // push hidden arg0 (closure_ref) followed by arg1..argN. self.push(Value::HeapRef(href)); for v in user_args.into_iter() { self.push(v); } let stack_base = self .operand_stack .len() .checked_sub(callee_param_slots) .ok_or_else(|| LogicalFrameEndingReason::Panic("Stack underflow".into()))?; // Allocate and zero-init local slots for _ in 0..callee_local_slots { self.operand_stack.push(Value::Null); } self.call_stack.push(CallFrame { return_pc: self.pc as u32, stack_base, func_idx: fn_id }); self.pc = callee_code_offset; } OpCode::PushConst => { let idx = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| { LogicalFrameEndingReason::Panic("Invalid constant index".into()) })?; self.push(val); } OpCode::PushI64 => { let val = instr .imm_i64() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; self.push(Value::Int64(val)); } OpCode::PushI32 => { let val = instr .imm_i32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; self.push(Value::Int32(val)); } OpCode::PushBounded => { let val = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; if val > 0xFFFF { return Err(self.trap( TRAP_OOB, opcode as u16, format!("Bounded value overflow: {} > 0xFFFF", val), start_pc as u32, )); } self.push(Value::Bounded(val)); } OpCode::PushF64 => { let val = instr .imm_f64() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; self.push(Value::Float(val)); } OpCode::PushBool => { let val = instr .imm_u8() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; self.push(Value::Boolean(val != 0)); } OpCode::Pop => { self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; } OpCode::PopN => { let n = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; for _ in 0..n { self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; } } OpCode::Dup => { let val = self.peek().map_err(|e| LogicalFrameEndingReason::Panic(e))?.clone(); self.push(val); } OpCode::Swap => { let a = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let b = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.push(a); self.push(b); } OpCode::Add => self.binary_op(opcode, start_pc as u32, |a, b| match (&a, &b) { (Value::String(_), _) | (_, Value::String(_)) => { Ok(Value::String(format!("{}{}", a.to_string(), b.to_string()))) } (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(*b))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))), (Value::Int32(a), Value::Int64(b)) => { Ok(Value::Int64((*a as i64).wrapping_add(*b))) } (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_add(*b as i64))), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)), (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)), (Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a + *b as f64)), (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a + *b as f64)), (Value::Bounded(a), Value::Bounded(b)) => { let res = a.saturating_add(*b); if res > 0xFFFF { Err(OpError::Trap( TRAP_OOB, format!("Bounded addition overflow: {} + {} = {}", a, b, res), )) } else { Ok(Value::Bounded(res)) } } _ => Err(OpError::Panic("Invalid types for ADD".into())), })?, OpCode::Sub => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_sub(b))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_sub(b))), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_sub(b))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_sub(b as i64))), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)), (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)), (Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a - b as f64)), (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)), (Value::Bounded(a), Value::Bounded(b)) => { if a < b { Err(OpError::Trap( TRAP_OOB, format!("Bounded subtraction underflow: {} - {} < 0", a, b), )) } else { Ok(Value::Bounded(a - b)) } } _ => Err(OpError::Panic("Invalid types for SUB".into())), })?, OpCode::Mul => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_mul(b))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_mul(b))), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_mul(b))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_mul(b as i64))), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)), (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)), (Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a * b as f64)), (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a * b as f64)), (Value::Bounded(a), Value::Bounded(b)) => { let res = a as u64 * b as u64; if res > 0xFFFF { Err(OpError::Trap( TRAP_OOB, format!("Bounded multiplication overflow: {} * {} = {}", a, b, res), )) } else { Ok(Value::Bounded(res as u32)) } } _ => Err(OpError::Panic("Invalid types for MUL".into())), })?, OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => { if b == 0 { return Err(OpError::Trap( TRAP_DIV_ZERO, "Integer division by zero".into(), )); } Ok(Value::Int32(a / b)) } (Value::Int64(a), Value::Int64(b)) => { if b == 0 { return Err(OpError::Trap( TRAP_DIV_ZERO, "Integer division by zero".into(), )); } Ok(Value::Int64(a / b)) } (Value::Int32(a), Value::Int64(b)) => { if b == 0 { return Err(OpError::Trap( TRAP_DIV_ZERO, "Integer division by zero".into(), )); } Ok(Value::Int64(a as i64 / b)) } (Value::Int64(a), Value::Int32(b)) => { if b == 0 { return Err(OpError::Trap( TRAP_DIV_ZERO, "Integer division by zero".into(), )); } Ok(Value::Int64(a / b as i64)) } (Value::Float(a), Value::Float(b)) => { if b == 0.0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into())); } Ok(Value::Float(a / b)) } (Value::Int32(a), Value::Float(b)) => { if b == 0.0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into())); } Ok(Value::Float(a as f64 / b)) } (Value::Float(a), Value::Int32(b)) => { if b == 0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into())); } Ok(Value::Float(a / b as f64)) } (Value::Int64(a), Value::Float(b)) => { if b == 0.0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into())); } Ok(Value::Float(a as f64 / b)) } (Value::Float(a), Value::Int64(b)) => { if b == 0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Float division by zero".into())); } Ok(Value::Float(a / b as f64)) } (Value::Bounded(a), Value::Bounded(b)) => { if b == 0 { return Err(OpError::Trap( TRAP_DIV_ZERO, "Bounded division by zero".into(), )); } Ok(Value::Bounded(a / b)) } _ => Err(OpError::Panic("Invalid types for DIV".into())), })?, OpCode::Mod => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => { if b == 0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer modulo by zero".into())); } Ok(Value::Int32(a % b)) } (Value::Int64(a), Value::Int64(b)) => { if b == 0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer modulo by zero".into())); } Ok(Value::Int64(a % b)) } (Value::Bounded(a), Value::Bounded(b)) => { if b == 0 { return Err(OpError::Trap(TRAP_DIV_ZERO, "Bounded modulo by zero".into())); } Ok(Value::Bounded(a % b)) } _ => Err(OpError::Panic("Invalid types for MOD".into())), })?, OpCode::BoundToInt => { let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; if let Value::Bounded(b) = val { self.push(Value::Int64(b as i64)); } else { return Err(LogicalFrameEndingReason::Panic( "Expected bounded for BOUND_TO_INT".into(), )); } } OpCode::IntToBoundChecked => { let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; let int_val = val.as_integer().ok_or_else(|| { LogicalFrameEndingReason::Panic( "Expected integer for INT_TO_BOUND_CHECKED".into(), ) })?; if !(0..=0xFFFF).contains(&int_val) { return Err(self.trap( TRAP_OOB, OpCode::IntToBoundChecked as u16, format!("Integer to bounded conversion out of range: {}", int_val), start_pc as u32, )); } self.push(Value::Bounded(int_val as u32)); } OpCode::Eq => { self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))? } OpCode::Neq => { self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))? } OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o == std::cmp::Ordering::Less)) .ok_or_else(|| OpError::Panic("Invalid types for LT".into())) })?, OpCode::Gt => self.binary_op(opcode, start_pc as u32, |a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o == std::cmp::Ordering::Greater)) .ok_or_else(|| OpError::Panic("Invalid types for GT".into())) })?, OpCode::Lte => self.binary_op(opcode, start_pc as u32, |a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o != std::cmp::Ordering::Greater)) .ok_or_else(|| OpError::Panic("Invalid types for LTE".into())) })?, OpCode::Gte => self.binary_op(opcode, start_pc as u32, |a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o != std::cmp::Ordering::Less)) .ok_or_else(|| OpError::Panic("Invalid types for GTE".into())) })?, OpCode::And => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a && b)), _ => Err(OpError::Panic("Invalid types for AND".into())), })?, OpCode::Or => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a || b)), _ => Err(OpError::Panic("Invalid types for OR".into())), })?, OpCode::Not => { let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; if let Value::Boolean(b) = val { self.push(Value::Boolean(!b)); } else { return Err(LogicalFrameEndingReason::Panic("Invalid type for NOT".into())); } } OpCode::BitAnd => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a & b)), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a & b)), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) & b)), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a & (b as i64))), _ => Err(OpError::Panic("Invalid types for BitAnd".into())), })?, OpCode::BitOr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a | b)), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a | b)), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) | b)), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a | (b as i64))), _ => Err(OpError::Panic("Invalid types for BitOr".into())), })?, OpCode::BitXor => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a ^ b)), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a ^ b)), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) ^ b)), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a ^ (b as i64))), _ => Err(OpError::Panic("Invalid types for BitXor".into())), })?, OpCode::Shl => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), (Value::Int32(a), Value::Int64(b)) => { Ok(Value::Int64((a as i64).wrapping_shl(b as u32))) } (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), _ => Err(OpError::Panic("Invalid types for Shl".into())), })?, OpCode::Shr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), (Value::Int32(a), Value::Int64(b)) => { Ok(Value::Int64((a as i64).wrapping_shr(b as u32))) } (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), _ => Err(OpError::Panic("Invalid types for Shr".into())), })?, OpCode::Neg => { let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; match val { Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())), Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())), Value::Float(a) => self.push(Value::Float(-a)), _ => { return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into())); } } } OpCode::GetGlobal => { let idx = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let val = self.globals.get(idx).cloned().ok_or_else(|| { LogicalFrameEndingReason::Panic("Invalid global index".into()) })?; self.push(val); } OpCode::SetGlobal => { let idx = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; if idx >= self.globals.len() { self.globals.resize(idx + 1, Value::Null); } self.globals[idx] = val; } OpCode::GetLocal => { let slot = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let frame = self.call_stack.last().ok_or_else(|| { LogicalFrameEndingReason::Panic("No active call frame".into()) })?; let func = &self.program.functions[frame.func_idx]; crate::local_addressing::check_local_slot( func, slot, opcode as u16, start_pc as u32, ) .map_err(|trap_info| { self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc) })?; let stack_idx = crate::local_addressing::local_index(frame, slot); let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| { LogicalFrameEndingReason::Panic( "Internal error: validated local slot not found in stack".into(), ) })?; self.push(val); } OpCode::SetLocal => { let slot = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let frame = self.call_stack.last().ok_or_else(|| { LogicalFrameEndingReason::Panic("No active call frame".into()) })?; let func = &self.program.functions[frame.func_idx]; crate::local_addressing::check_local_slot( func, slot, opcode as u16, start_pc as u32, ) .map_err(|trap_info| { self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc) })?; let stack_idx = crate::local_addressing::local_index(frame, slot); self.operand_stack[stack_idx] = val; } OpCode::Call => { let func_id = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let callee = self.program.functions.get(func_id).ok_or_else(|| { self.trap( TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {}", func_id), start_pc as u32, ) })?; if self.operand_stack.len() < callee.param_slots as usize { return Err(LogicalFrameEndingReason::Panic(format!( "Stack underflow during CALL to func {}: expected at least {} arguments, got {}", func_id, callee.param_slots, self.operand_stack.len() ))); } let stack_base = self.operand_stack.len() - callee.param_slots as usize; // Allocate and zero-init local_slots for _ in 0..callee.local_slots { self.operand_stack.push(Value::Null); } self.call_stack.push(CallFrame { return_pc: self.pc as u32, stack_base, func_idx: func_id, }); self.pc = callee.code_offset as usize; } OpCode::Ret => { let frame = self.call_stack.pop().ok_or_else(|| { LogicalFrameEndingReason::Panic("Call stack underflow".into()) })?; let func = &self.program.functions[frame.func_idx]; let return_slots = func.return_slots as usize; let current_height = self.operand_stack.len(); let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots; if current_height != expected_height { return Err(self.trap(TRAP_BAD_RET_SLOTS, opcode as u16, format!( "Incorrect stack height at RET in func {}: expected {} slots (stack_base={} + params={} + locals={} + returns={}), got {}", frame.func_idx, expected_height, frame.stack_base, func.param_slots, func.local_slots, return_slots, current_height ), start_pc as u32)); } // Copy return values (preserving order: pop return_slots values, then reverse to push back) let mut return_vals = Vec::with_capacity(return_slots); for _ in 0..return_slots { return_vals.push(self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?); } return_vals.reverse(); self.operand_stack.truncate(frame.stack_base); for val in return_vals { self.push(val); } self.pc = frame.return_pc as usize; } OpCode::Syscall => { let pc_at_syscall = start_pc as u32; let id = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let syscall = prometeu_hal::syscalls::Syscall::from_u32(id).ok_or_else(|| { self.trap( TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!("Unknown syscall: 0x{:08X}", id), pc_at_syscall, ) })?; // Capability check before any side effects or argument consumption. let meta = prometeu_hal::syscalls::meta_for(syscall); if (self.capabilities & meta.caps) != meta.caps { return Err(self.trap( TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!( "Missing capability for syscall {} (required=0x{:X})", syscall.name(), meta.caps ), pc_at_syscall, )); } let args_count = syscall.args_count(); let mut args = Vec::with_capacity(args_count); for _ in 0..args_count { let v = self.pop().map_err(|_e| { self.trap( TRAP_STACK_UNDERFLOW, OpCode::Syscall as u16, "Syscall argument stack underflow".to_string(), pc_at_syscall, ) })?; args.push(v); } args.reverse(); let stack_height_before = self.operand_stack.len(); let mut ret = crate::HostReturn::new(&mut self.operand_stack); native.syscall(id, &args, &mut ret, ctx).map_err(|fault| match fault { VmFault::Trap(code, msg) => { self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall) } VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg), VmFault::Unavailable => { LogicalFrameEndingReason::Panic("Host feature unavailable".into()) } })?; let stack_height_after = self.operand_stack.len(); let results_pushed = stack_height_after - stack_height_before; if results_pushed != syscall.results_count() { return Err(LogicalFrameEndingReason::Panic(format!( "Syscall {} (0x{:08X}) results mismatch: expected {}, got {}", syscall.name(), id, syscall.results_count(), results_pushed ))); } } OpCode::FrameSync => { // Marks the logical end of a frame: consume cycles and signal to the driver self.cycles += OpCode::FrameSync.cycles(); self.handle_safepoint(); return Err(LogicalFrameEndingReason::FrameSync); } } // Apply the instruction cost to the cycle counter self.cycles += opcode.cycles(); Ok(()) } /// Perform safepoint duties that occur at logical frame boundaries. /// Runs GC if thresholds are reached, clears cooperative yield flag, /// and advances the logical tick counter. fn handle_safepoint(&mut self) { // 1) GC Safepoint: only at FRAME_SYNC-like boundaries if self.gc_alloc_threshold > 0 { let live_now = self.heap.len(); let since_last = live_now.saturating_sub(self.last_gc_live_count); if since_last >= self.gc_alloc_threshold { // Collect GC roots from VM state struct CollectRoots(Vec); impl crate::roots::RootVisitor for CollectRoots { fn visit_heap_ref(&mut self, r: prometeu_bytecode::HeapRef) { self.0.push(r); } } let mut collector = CollectRoots(Vec::new()); self.visit_roots(&mut collector); // Add current coroutine and all suspended (ready/sleeping) coroutines as GC roots if let Some(cur) = self.current_coro { collector.0.push(cur); } let mut coro_roots = self.heap.suspended_coroutine_handles(); collector.0.append(&mut coro_roots); // Run mark-sweep self.heap.mark_from_roots(collector.0); self.heap.sweep(); // Update baseline for next cycles self.last_gc_live_count = self.heap.len(); } } // 2) Advance logical tick and wake sleepers self.current_tick = self.current_tick.wrapping_add(1); self.scheduler.wake_ready(self.current_tick); // 3) Apply pending transitions for the current coroutine (yield/sleep/finished) let mut switched_out = false; if let Some(cur) = self.current_coro { // Handle sleep request if let Some(wake) = self.sleep_requested_until.take() { if let Some(co) = self.heap.coroutine_data_mut(cur) { // Save execution context into the coroutine object co.pc = self.pc; co.stack = std::mem::take(&mut self.operand_stack); co.frames = std::mem::take(&mut self.call_stack); co.state = CoroutineState::Sleeping; co.wake_tick = wake; } self.scheduler.sleep_until(cur, wake); self.current_coro = None; self.scheduler.clear_current(); switched_out = true; } else if self.yield_requested { if let Some(co) = self.heap.coroutine_data_mut(cur) { co.pc = self.pc; co.stack = std::mem::take(&mut self.operand_stack); co.frames = std::mem::take(&mut self.call_stack); co.state = CoroutineState::Ready; } self.scheduler.enqueue_ready(cur); self.current_coro = None; self.scheduler.clear_current(); switched_out = true; } else if self.halted || self.pc >= self.program.rom.len() { // Current finished; save final context and mark Finished if let Some(co) = self.heap.coroutine_data_mut(cur) { co.pc = self.pc; co.stack = std::mem::take(&mut self.operand_stack); co.frames = std::mem::take(&mut self.call_stack); co.state = CoroutineState::Finished; } self.current_coro = None; self.scheduler.clear_current(); switched_out = true; } else { // Stays running; nothing to do } } // 4) Select next coroutine if needed if self.current_coro.is_none() { if let Some(next) = self.scheduler.dequeue_next() { // Load next context into the VM if let Some(co) = self.heap.coroutine_data_mut(next) { self.pc = co.pc; self.operand_stack = std::mem::take(&mut co.stack); self.call_stack = std::mem::take(&mut co.frames); co.state = CoroutineState::Running; } self.current_coro = Some(next); self.scheduler.set_current(self.current_coro); } else { // Nothing ready now. If there are sleeping coroutines, we keep VM idle until next frame tick. // If there are no sleeping coroutines either (i.e., all finished), we can halt deterministically. if switched_out && !self.scheduler.has_sleeping() { self.halted = true; } } } else { // Keep current as scheduler current for observability self.scheduler.set_current(self.current_coro); } // 5) Clear cooperative yield request at the safepoint boundary. self.yield_requested = false; } // /// Save the currently running VM execution context back into its coroutine object. // /// Must be called only at safepoints. // fn save_current_context_into_coroutine(&mut self) { // if let Some(cur) = self.current_coro { // if let Some(co) = self.heap.coroutine_data_mut(cur) { // co.pc = self.pc; // co.stack = std::mem::take(&mut self.operand_stack); // co.frames = std::mem::take(&mut self.call_stack); // } // } // } // /// Load a coroutine context from heap into the VM runtime state. // /// Must be called only at safepoints. // fn load_coroutine_context_into_vm(&mut self, coro: HeapRef) { // if let Some(co) = self.heap.coroutine_data_mut(coro) { // self.pc = co.pc; // self.operand_stack = std::mem::take(&mut co.stack); // self.call_stack = std::mem::take(&mut co.frames); // co.state = CoroutineState::Running; // } // self.current_coro = Some(coro); // self.scheduler.set_current(self.current_coro); // } pub fn trap( &self, code: u32, opcode: u16, message: String, pc: u32, ) -> LogicalFrameEndingReason { LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc)) } pub fn push(&mut self, val: Value) { self.operand_stack.push(val); } pub fn pop(&mut self) -> Result { self.operand_stack.pop().ok_or("Stack underflow".into()) } pub fn pop_number(&mut self) -> Result { let val = self.pop()?; val.as_float().ok_or_else(|| "Expected number".into()) } pub fn pop_integer(&mut self) -> Result { let val = self.pop()?; if let Value::Boolean(b) = val { return Ok(if b { 1 } else { 0 }); } val.as_integer().ok_or_else(|| "Expected integer".into()) } pub fn peek(&self) -> Result<&Value, String> { self.operand_stack.last().ok_or("Stack underflow".into()) } fn binary_op( &mut self, opcode: OpCode, start_pc: u32, f: F, ) -> Result<(), LogicalFrameEndingReason> where F: FnOnce(Value, Value) -> Result, { let b = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let a = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; match f(a, b) { Ok(res) => { self.push(res); Ok(()) } Err(OpError::Trap(code, msg)) => Err(self.trap(code, opcode as u16, msg, start_pc)), Err(OpError::Panic(msg)) => Err(LogicalFrameEndingReason::Panic(msg)), } } /// Visit all GC roots reachable from the VM state. /// This includes: /// - Entire operand stack values /// - Locals/args in each call frame (derived from `stack_base` and function layout) /// - Global variables pub fn visit_roots(&self, visitor: &mut V) { // 1) Operand stack (all values are roots) for v in &self.operand_stack { visit_value_for_roots(v, visitor); } // 2) Call frames: iterate locals/args range for each frame for frame in &self.call_stack { if let Some(func_meta) = self.program.functions.get(frame.func_idx) { let start = frame.stack_base; let frame_slots = (func_meta.param_slots as usize) + (func_meta.local_slots as usize); let mut end = start.saturating_add(frame_slots); // Clamp to current stack height just in case if end > self.operand_stack.len() { end = self.operand_stack.len(); } for i in start..end { if let Some(v) = self.operand_stack.get(i) { visit_value_for_roots(v, visitor); } } } } // 3) Globals for g in &self.globals { visit_value_for_roots(g, visitor); } } } #[cfg(test)] mod tests { use super::*; fn new_test_vm(rom: Vec, constant_pool: Vec) -> VirtualMachine { let rom_len = rom.len() as u32; let mut vm = VirtualMachine::new(rom, constant_pool); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom_len, ..Default::default() }]); // Ensure tests start with a properly initialized main coroutine at func 0 vm.prepare_call("0"); vm } use crate::HostReturn; use prometeu_bytecode::{FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW}; use prometeu_hal::expect_int; struct MockNative; impl NativeInterface for MockNative { fn syscall( &mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext, ) -> Result<(), VmFault> { Ok(()) } } #[test] fn sleep_delays_execution_by_ticks() { let mut native = MockNative; let mut ctx = HostContext::new(None); // Program: // SLEEP 2 // PUSH_I32 123 // FRAME_SYNC // HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); rom.extend_from_slice(&(2u32).to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&123i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom, vec![]); // Frame 1: executing SLEEP 2 will force a frame end and advance tick to 1 let rep1 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok"); assert!(matches!(rep1.reason, LogicalFrameEndingReason::FrameSync)); assert!(vm.operand_stack.is_empty()); assert_eq!(vm.current_tick, 1); // Frame 2: still sleeping (tick 1 < wake 3), immediate FrameSync, tick -> 2 let rep2 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok"); assert!(matches!(rep2.reason, LogicalFrameEndingReason::FrameSync)); // In the per-coroutine model, the VM may keep current context intact across idle frames; // we must not observe any new values pushed before wake. Stack height must be unchanged. assert_eq!(vm.operand_stack.len(), 0); assert_eq!(vm.current_tick, 2); // Frame 3: still sleeping (tick 2 < wake 3), immediate FrameSync, tick -> 3 let rep3 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok"); assert!(matches!(rep3.reason, LogicalFrameEndingReason::FrameSync)); assert_eq!(vm.operand_stack.len(), 0); assert_eq!(vm.current_tick, 3); // Frame 4: wake condition met (current_tick >= wake), execute PUSH_I32 then FRAME_SYNC let rep4 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok"); assert!(matches!(rep4.reason, LogicalFrameEndingReason::FrameSync)); // Value should now be on the stack assert_eq!(vm.peek().unwrap(), &Value::Int32(123)); // Next frame should hit HALT without errors let res = vm.run_budget(100, &mut native, &mut ctx); assert!(res.is_ok()); } #[test] fn test_deterministic_pc_and_tick_trace_across_runs() { // Program: // PUSH_I32 1; YIELD; FrameSync; // PUSH_I32 2; YIELD; FrameSync; // PUSH_I32 3; FrameSync; HALT // We collect (pc, tick, stack_height) after each run_budget slice and // compare two independent VMs initialized from the same ROM. let mut rom = Vec::new(); // PUSH 1 rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&1i32.to_le_bytes()); // YIELD + FrameSync rom.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); // PUSH 2 rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&2i32.to_le_bytes()); // YIELD + FrameSync rom.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); // PUSH 3 + FrameSync + HALT rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&3i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm1 = new_test_vm(rom.clone(), vec![]); let mut vm2 = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx1 = HostContext::new(None); let mut ctx2 = HostContext::new(None); let mut trace1 = Vec::new(); let mut trace2 = Vec::new(); // Run both VMs in lockstep slices until both halt for _ in 0..10 { if !vm1.halted { let rep = vm1.run_budget(4, &mut native, &mut ctx1).expect("vm1 ok"); trace1.push((vm1.pc, vm1.current_tick, vm1.operand_stack.len(), format!("{:?}", rep.reason))); } if !vm2.halted { let rep = vm2.run_budget(4, &mut native, &mut ctx2).expect("vm2 ok"); trace2.push((vm2.pc, vm2.current_tick, vm2.operand_stack.len(), format!("{:?}", rep.reason))); } if vm1.halted && vm2.halted { break; } } assert!(vm1.halted && vm2.halted, "Both VMs should reach HALT deterministically"); assert_eq!(trace1, trace2, "Per-slice traces must be identical across runs"); // Also verify final stack content deterministic assert_eq!(vm1.pop().unwrap(), Value::Int32(3)); assert_eq!(vm1.pop().unwrap(), Value::Int32(2)); assert_eq!(vm1.pop().unwrap(), Value::Int32(1)); assert!(vm1.operand_stack.is_empty()); assert_eq!(vm2.pop().unwrap(), Value::Int32(3)); assert_eq!(vm2.pop().unwrap(), Value::Int32(2)); assert_eq!(vm2.pop().unwrap(), Value::Int32(1)); assert!(vm2.operand_stack.is_empty()); } #[test] fn test_sleep_wake_determinism_across_runs() { // Program: // SLEEP 2; PUSH 7; FrameSync; HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); rom.extend_from_slice(&(2u32).to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&7i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut native = MockNative; let mut vm_a = new_test_vm(rom.clone(), vec![]); let mut vm_b = new_test_vm(rom.clone(), vec![]); let mut ctx_a = HostContext::new(None); let mut ctx_b = HostContext::new(None); let mut ticks_a = Vec::new(); let mut ticks_b = Vec::new(); // Slice 1 let ra1 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap(); let rb1 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap(); ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra1.reason))); ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb1.reason))); // Slice 2 let ra2 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap(); let rb2 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap(); ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra2.reason))); ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb2.reason))); // Slice 3 let ra3 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap(); let rb3 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap(); ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra3.reason))); ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb3.reason))); // Slice 4 (wakes and pushes) let ra4 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap(); let rb4 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap(); ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra4.reason))); ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb4.reason))); assert_eq!(ticks_a, ticks_b, "Sleep/wake slices must match across runs"); assert_eq!(vm_a.peek().unwrap(), &Value::Int32(7)); assert_eq!(vm_b.peek().unwrap(), &Value::Int32(7)); } #[test] fn test_gc_many_coroutines_and_wake_order_determinism() { use crate::heap::{CoroutineState}; use crate::object::ObjectKind; // ROM: FrameSync; FrameSync; Halt (two deterministic safepoints back-to-back) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.gc_alloc_threshold = 1; // force GC at first safepoint // Allocate many coroutine objects: half Ready, half Sleeping with differing wake ticks. let coro_count = 128u32; for i in 0..coro_count { let state = if i % 2 == 0 { CoroutineState::Ready } else { CoroutineState::Sleeping }; let wake = if state == CoroutineState::Sleeping { (i / 2) as u64 } else { 0 }; let _c = vm.heap.allocate_coroutine(0, state, wake, vec![], vec![]); // Also allocate a tiny byte object to increase GC pressure. let _b = vm.heap.allocate_object(ObjectKind::Bytes, &[i as u8]); } // Sanity: allocations present assert!(vm.heap.len() as u32 >= coro_count, "heap should contain coroutine objects and bytes"); let mut native = MockNative; let mut ctx = HostContext::new(None); // Reaching FrameSync should run GC; Ready/Sleeping coroutines are treated as roots, so // only unreferenced byte objects can be reclaimed. We just assert determinism: heap size // should be stable across two consecutive FrameSyncs with no new allocations in between. let before = vm.heap.len(); match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } let after_first = vm.heap.len(); match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } let after_second = vm.heap.len(); // GC effect should be deterministic and idempotent at steady state assert!(after_first <= before); assert_eq!(after_second, after_first); } // fn test_arithmetic_chain() { // let mut native = MockNative; // let mut ctx = HostContext::new(None); // // // (10 + 20) * 2 / 5 % 4 = 12 * 2 / 5 % 4 = 60 / 5 % 4 = 12 % 4 = 0 // // wait: (10 + 20) = 30. 30 * 2 = 60. 60 / 5 = 12. 12 % 4 = 0. // let mut rom = Vec::new(); // rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // rom.extend_from_slice(&10i32.to_le_bytes()); // rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // rom.extend_from_slice(&20i32.to_le_bytes()); // rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes()); // rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // rom.extend_from_slice(&2i32.to_le_bytes()); // rom.extend_from_slice(&(OpCode::Mul as u16).to_le_bytes()); // rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // rom.extend_from_slice(&5i32.to_le_bytes()); // rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes()); // rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // rom.extend_from_slice(&4i32.to_le_bytes()); // rom.extend_from_slice(&(OpCode::Mod as u16).to_le_bytes()); // rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); // // let mut vm = new_test_vm(rom.clone(), vec![]); // vm.run_budget(100, &mut native, &mut ctx).unwrap(); // // assert_eq!(vm.pop().unwrap(), Value::Int32(0)); // } #[test] fn test_div_by_zero_trap() { let mut native = MockNative; let mut ctx = HostContext::new(None); let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&10i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&0i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_DIV_ZERO); assert_eq!(trap.opcode, OpCode::Div as u16); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_int_to_bound_checked_trap() { let mut native = MockNative; let mut ctx = HostContext::new(None); let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&70000i32.to_le_bytes()); // > 65535 rom.extend_from_slice(&(OpCode::IntToBoundChecked as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_OOB); assert_eq!(trap.opcode, OpCode::IntToBoundChecked as u16); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_bounded_add_overflow_trap() { let mut native = MockNative; let mut ctx = HostContext::new(None); let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushBounded as u16).to_le_bytes()); rom.extend_from_slice(&60000u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushBounded as u16).to_le_bytes()); rom.extend_from_slice(&10000u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_OOB); assert_eq!(trap.opcode, OpCode::Add as u16); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_comparisons_polymorphic() { let mut native = MockNative; let mut ctx = HostContext::new(None); // 10 < 20.5 (true) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&10i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes()); rom.extend_from_slice(&20.5f64.to_le_bytes()); rom.extend_from_slice(&(OpCode::Lt as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Boolean(true)); } #[test] fn test_push_i64_immediate() { let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); rom.extend_from_slice(&42i64.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.peek().unwrap(), &Value::Int64(42)); } #[test] fn test_push_f64_immediate() { let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes()); rom.extend_from_slice(&3.14f64.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.peek().unwrap(), &Value::Float(3.14)); } #[test] fn test_push_bool_immediate() { let mut rom = Vec::new(); // True rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); rom.push(1); // False rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); rom.push(0); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); // Push true assert_eq!(vm.peek().unwrap(), &Value::Boolean(true)); vm.step(&mut native, &mut ctx).unwrap(); // Push false assert_eq!(vm.peek().unwrap(), &Value::Boolean(false)); } #[test] fn test_push_const_string() { let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let cp = vec![Value::String("hello".into())]; let mut vm = new_test_vm(rom, cp); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.peek().unwrap(), &Value::String("hello".into())); } #[test] fn test_call_ret_scope_separation() { let mut rom = Vec::new(); // entrypoint: // PUSH_I64 10 // CALL func_id 1 // HALT rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); rom.extend_from_slice(&10i64.to_le_bytes()); rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); // func_id 1 rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let func_addr = rom.len(); // func: // PUSH_I64 20 // GET_LOCAL 0 -- should be 10 (arg) // ADD -- 10 + 20 = 30 // SET_LOCAL 0 -- store result in local 0 (the arg slot) // GET_LOCAL 0 -- read 30 back // RET rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); rom.extend_from_slice(&20i64.to_le_bytes()); rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let functions = vec![ FunctionMeta { code_offset: 0, code_len: func_addr as u32, ..Default::default() }, FunctionMeta { code_offset: func_addr as u32, code_len: (rom.len() - func_addr) as u32, param_slots: 1, return_slots: 1, ..Default::default() }, ]; let mut vm = VirtualMachine { program: ProgramImage::new( rom, vec![], functions, None, std::collections::HashMap::new(), ), ..Default::default() }; vm.prepare_call("0"); let mut native = MockNative; let mut ctx = HostContext::new(None); // Run until Halt let mut steps = 0; while !vm.halted && steps < 100 { vm.step(&mut native, &mut ctx).unwrap(); steps += 1; } assert!(vm.halted); assert_eq!(vm.pop_integer().unwrap(), 30); assert_eq!(vm.operand_stack.len(), 0); assert_eq!(vm.call_stack.len(), 1); // Scope frames removed: no scope stack to assert on } #[test] fn test_ret_mandatory_value() { let mut rom = Vec::new(); // entrypoint: CALL func_id 1; HALT rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); // func_id 1 rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let func_addr = rom.len(); // func: RET (SEM VALOR ANTES) rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let functions = vec![ FunctionMeta { code_offset: 0, code_len: func_addr as u32, ..Default::default() }, FunctionMeta { code_offset: func_addr as u32, code_len: (rom.len() - func_addr) as u32, param_slots: 0, return_slots: 1, ..Default::default() }, ]; let mut vm = VirtualMachine { program: ProgramImage::new( rom, vec![], functions, None, std::collections::HashMap::new(), ), ..Default::default() }; vm.prepare_call("0"); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); // CALL let res = vm.step(&mut native, &mut ctx); // RET -> should fail assert!(res.is_err()); match res.unwrap_err() { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_BAD_RET_SLOTS); } _ => panic!("Expected Trap(TRAP_BAD_RET_SLOTS)"), } // Agora com valor de retorno let mut rom2 = Vec::new(); rom2.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom2.extend_from_slice(&1u32.to_le_bytes()); rom2.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let func_addr2 = rom2.len(); rom2.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); rom2.extend_from_slice(&123i64.to_le_bytes()); rom2.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let functions2 = vec![ FunctionMeta { code_offset: 0, code_len: func_addr2 as u32, ..Default::default() }, FunctionMeta { code_offset: func_addr2 as u32, code_len: (rom2.len() - func_addr2) as u32, param_slots: 0, return_slots: 1, ..Default::default() }, ]; let mut vm2 = VirtualMachine { program: ProgramImage::new( rom2, vec![], functions2, None, std::collections::HashMap::new(), ), ..Default::default() }; vm2.prepare_call("0"); vm2.step(&mut native, &mut ctx).unwrap(); // CALL vm2.step(&mut native, &mut ctx).unwrap(); // PUSH_I64 vm2.step(&mut native, &mut ctx).unwrap(); // RET assert_eq!(vm2.operand_stack.len(), 1); assert_eq!(vm2.pop().unwrap(), Value::Int64(123)); } // Scope tests removed under PR-2.1 (scope frames eliminated) #[test] fn test_push_i32() { let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); } #[test] fn test_bitwise_promotion() { let mut native = MockNative; let mut ctx = HostContext::new(None); // i32 & i32 -> i32 let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&0xF0i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&0x0Fi32.to_le_bytes()); rom.extend_from_slice(&(OpCode::BitAnd as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Int32(0)); // i32 | i64 -> i64 let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&0xF0i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); rom.extend_from_slice(&0x0Fi64.to_le_bytes()); rom.extend_from_slice(&(OpCode::BitOr as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Int64(0xFF)); } #[test] fn test_comparisons_lte_gte() { let mut native = MockNative; let mut ctx = HostContext::new(None); // 10 <= 20 (true) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&10i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&20i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Lte as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Boolean(true)); // 20 >= 20 (true) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&20i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&20i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Gte as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Boolean(true)); } #[test] fn test_negation() { let mut native = MockNative; let mut ctx = HostContext::new(None); let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Neg as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.step(&mut native, &mut ctx).unwrap(); vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Int32(-42)); } #[test] fn test_jmp_if_true() { let mut native = MockNative; let mut ctx = HostContext::new(None); // Corrected Calculations: // 0-1: PushBool // 2: 1 (u8) // 3-4: JmpIfTrue // 5-8: addr (u32) // 9-10: Halt (Offset 9) // 11-12: PushI32 (Offset 11) // 13-16: 100 (i32) // 17-18: Halt let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); rom.push(1); rom.extend_from_slice(&(OpCode::JmpIfTrue as u16).to_le_bytes()); rom.extend_from_slice(&(11u32).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); // Offset 9 rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // Offset 11 rom.extend_from_slice(&100i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.step(&mut native, &mut ctx).unwrap(); // PushBool vm.step(&mut native, &mut ctx).unwrap(); // JmpIfTrue assert_eq!(vm.pc, 11); vm.step(&mut native, &mut ctx).unwrap(); // PushI32 assert_eq!(vm.pop().unwrap(), Value::Int32(100)); } #[test] fn test_trap_opcode() { let mut native = MockNative; let mut ctx = HostContext::new(None); let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Trap as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Breakpoint); assert_eq!(vm.pc, 8); // PushI32 (6 bytes) + Trap (2 bytes) assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); } #[test] fn test_pop_n_opcode() { let mut native = MockNative; let mut ctx = HostContext::new(None); let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&1i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&2i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&3i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PopN as u16).to_le_bytes()); rom.extend_from_slice(&2u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Int32(1)); assert!(vm.pop().is_err()); // Stack should be empty } #[test] fn test_entry_point_ret_with_prepare_call() { // PushI32 0 (0x17), then Ret (0x51) let rom = vec![ 0x17, 0x00, // PushI32 0x00, 0x00, 0x00, 0x00, // value 0 0x11, 0x00, // Pop 0x51, 0x00, // Ret ]; let mut vm = VirtualMachine::new(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); let mut ctx = HostContext::new(None); struct TestNative; impl NativeInterface for TestNative { fn syscall( &mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext, ) -> Result<(), VmFault> { Ok(()) } } let mut native = TestNative; vm.prepare_call("0"); let result = vm.run_budget(100, &mut native, &mut ctx).expect("VM run failed"); assert_eq!(result.reason, LogicalFrameEndingReason::EndOfRom); } #[test] fn test_syscall_abi_multi_slot_return() { let rom = vec![ 0x70, 0x00, // Syscall + Reserved 0x01, 0x00, 0x00, 0x00, // Syscall ID 1 ]; struct MultiReturnNative; impl NativeInterface for MultiReturnNative { fn syscall( &mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext, ) -> Result<(), VmFault> { ret.push_bool(true); ret.push_int(42); ret.push_bounded(255)?; Ok(()) } } let mut vm = VirtualMachine::new(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); let mut native = MultiReturnNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); // Ensure we have SYSTEM capability so we pass capability gate vm.set_capabilities(prometeu_hal::syscalls::caps::SYSTEM); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); // Under PR5, VM enforces return-slot count based on SyscallMeta during syscall // execution. A mismatch yields a Panic with a descriptive message. match report.reason { LogicalFrameEndingReason::Panic(msg) => { assert!(msg.contains("results mismatch")); } _ => panic!("Expected Panic with results mismatch, got {:?}", report.reason), } } #[test] fn test_syscall_abi_void_return() { let rom = vec![ 0x70, 0x00, // Syscall + Reserved 0x01, 0x00, 0x00, 0x00, // Syscall ID 1 ]; struct VoidReturnNative; impl NativeInterface for VoidReturnNative { fn syscall( &mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext, ) -> Result<(), VmFault> { Ok(()) } } let mut vm = VirtualMachine::new(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); let mut native = VoidReturnNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); vm.operand_stack.push(Value::Int32(100)); vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Int32(100)); assert!(vm.operand_stack.is_empty()); } #[test] fn test_syscall_arg_type_mismatch_trap() { // GfxClear (0x1001) takes 1 argument let rom = vec![ 0x16, 0x00, // PushBool + Reserved 0x01, // value 1 (true) 0x70, 0x00, // Syscall + Reserved 0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001 ]; struct ArgCheckNative; impl NativeInterface for ArgCheckNative { fn syscall( &mut self, _id: u32, args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext, ) -> Result<(), VmFault> { expect_int(args, 0)?; Ok(()) } } let mut vm = VirtualMachine::new(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); let mut native = ArgCheckNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); // Ensure we have GFX capability so we reach type checking inside native handler vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_TYPE); assert_eq!(trap.opcode, OpCode::Syscall as u16); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_invalid_syscall_trap() { let rom = vec![ 0x70, 0x00, // Syscall + Reserved 0xEF, 0xBE, 0xAD, 0xDE, // 0xDEADBEEF ]; let mut vm = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); // Grant GFX capability so arg underflow is checked (capability gate is first) vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_INVALID_SYSCALL); assert_eq!(trap.opcode, OpCode::Syscall as u16); assert!(trap.message.contains("Unknown syscall")); assert_eq!(trap.pc, 0); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_syscall_arg_underflow_trap() { // GfxClear (0x1001) expects 1 arg let rom = vec![ 0x70, 0x00, // Syscall + Reserved 0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001 ]; let mut vm = new_test_vm(rom.clone(), vec![]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); // Grant GFX capability so arg underflow is checked (capability gate is first) vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_STACK_UNDERFLOW); assert_eq!(trap.opcode, OpCode::Syscall as u16); assert!(trap.message.contains("underflow")); assert_eq!(trap.pc, 0); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_syscall_missing_capability_trap() { // Program: directly call GfxClear (0x1001). We check caps before args, so no underflow. let rom = vec![ 0x70, 0x00, // Syscall + Reserved 0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001 (LE) ]; let mut vm = new_test_vm(rom.clone(), vec![]); // Remove all capabilities vm.set_capabilities(0); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_INVALID_SYSCALL); assert_eq!(trap.opcode, OpCode::Syscall as u16); assert!(trap.message.contains("Missing capability")); assert_eq!(trap.pc, 0); } other => panic!("Expected Trap, got {:?}", other), } } #[test] fn test_syscall_with_capability_success() { // Program: push arg 0; call GfxClear (0x1001) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&0i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes()); rom.extend_from_slice(&0x1001u32.to_le_bytes()); // GfxClear let mut vm = new_test_vm(rom.clone(), vec![]); // Grant only GFX capability vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); // Any non-trap outcome is considered success here match report.reason { LogicalFrameEndingReason::Trap(trap) => panic!("Unexpected trap: {:?}", trap), _ => {} } } #[test] fn test_syscall_results_count_mismatch_panic() { // GfxClear565 (0x1010) expects 0 results let rom = vec![ 0x17, 0x00, // PushI32 0x00, 0x00, 0x00, 0x00, // value 0 0x70, 0x00, // Syscall + Reserved 0x10, 0x10, 0x00, 0x00, // Syscall ID 0x1010 ]; struct BadNative; impl NativeInterface for BadNative { fn syscall( &mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext, ) -> Result<(), VmFault> { // Wrong: GfxClear565 is void but we push something ret.push_int(42); Ok(()) } } let mut vm = new_test_vm(rom.clone(), vec![]); // Grant GFX capability so results mismatch path is exercised vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); let mut native = BadNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Panic(msg) => assert!(msg.contains("results mismatch")), _ => panic!("Expected Panic, got {:?}", report.reason), } } #[test] fn test_host_return_bounded_overflow_trap() { let mut stack = Vec::new(); let mut ret = HostReturn::new(&mut stack); let res = ret.push_bounded(65536); assert!(res.is_err()); match res.err().unwrap() { VmFault::Trap(code, _) => { assert_eq!(code, TRAP_OOB); } _ => panic!("Expected Trap"), } } #[test] fn test_loader_hardening_invalid_magic() { let mut vm = VirtualMachine::default(); let res = vm.initialize(vec![0, 0, 0, 0], ""); assert_eq!(res, Err(VmInitError::InvalidFormat)); // VM should remain empty assert_eq!(vm.program.rom.len(), 0); } #[test] fn test_loader_hardening_unsupported_version() { let mut vm = VirtualMachine::default(); let mut header = vec![0u8; 32]; header[0..4].copy_from_slice(b"PBS\0"); header[4..6].copy_from_slice(&1u16.to_le_bytes()); // version 1 (unsupported) let res = vm.initialize(header, ""); assert_eq!(res, Err(VmInitError::UnsupportedFormat)); } #[test] fn test_loader_hardening_malformed_pbs_v0() { let mut vm = VirtualMachine::default(); let mut header = vec![0u8; 32]; header[0..4].copy_from_slice(b"PBS\0"); header[8..12].copy_from_slice(&1u32.to_le_bytes()); // 1 section claimed but none provided let res = vm.initialize(header, ""); match res { Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {} _ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res), } } #[test] fn test_loader_hardening_entrypoint_not_found() { let mut vm = VirtualMachine::default(); // Valid empty PBS v0 module let mut header = vec![0u8; 32]; header[0..4].copy_from_slice(b"PBS\0"); // Try to initialize with numeric entrypoint 10 (out of bounds for empty ROM) let res = vm.initialize(header, "10"); assert_eq!(res, Err(VmInitError::EntrypointNotFound)); // VM state should not be updated assert_eq!(vm.pc, 0); assert_eq!(vm.program.rom.len(), 0); } #[test] fn test_loader_hardening_successful_init() { let mut vm = VirtualMachine::default(); vm.pc = 123; // Pollution let mut header = vec![0u8; 32]; header[0..4].copy_from_slice(b"PBS\0"); let res = vm.initialize(header, ""); assert!(res.is_ok()); assert_eq!(vm.pc, 0); assert_eq!(vm.program.rom.len(), 0); assert_eq!(vm.cycles, 0); } #[test] fn test_calling_convention_add() { let mut native = MockNative; let mut ctx = HostContext::new(None); // F0 (entry): // PUSH_I32 10 // PUSH_I32 20 // CALL 1 (add) // HALT // F1 (add): // GET_LOCAL 0 (a) // GET_LOCAL 1 (b) // ADD // RET (1 slot) let mut rom = Vec::new(); // F0 let f0_start = 0; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&10i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&20i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; // F1 let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 2, return_slots: 1, ..Default::default() }, ]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); assert_eq!(vm.operand_stack.last().unwrap(), &Value::Int32(30)); } #[test] fn test_calling_convention_multi_slot_return() { let mut native = MockNative; let mut ctx = HostContext::new(None); // F0: // CALL 1 // HALT // F1: // PUSH_I32 100 // PUSH_I32 200 // RET (2 slots) let mut rom = Vec::new(); // F0 let f0_start = 0; rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; // F1 let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&100i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&200i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 0, return_slots: 2, ..Default::default() }, ]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); // Stack should be [100, 200] assert_eq!(vm.operand_stack.len(), 2); assert_eq!(vm.operand_stack[0], Value::Int32(100)); assert_eq!(vm.operand_stack[1], Value::Int32(200)); } #[test] fn test_calling_convention_void_call() { let mut native = MockNative; let mut ctx = HostContext::new(None); // F0: // PUSH_I32 42 // CALL 1 // HALT // F1: // POP // RET (0 slots) let mut rom = Vec::new(); let f0_start = 0; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 0, ..Default::default() }, ]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); assert_eq!(vm.operand_stack.len(), 0); } #[test] fn test_trap_invalid_func() { let mut native = MockNative; let mut ctx = HostContext::new(None); // CALL 99 (invalid) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&99u32.to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_INVALID_FUNC); assert_eq!(trap.opcode, OpCode::Call as u16); } _ => panic!("Expected Trap(TRAP_INVALID_FUNC), got {:?}", report.reason), } } #[test] fn test_trap_bad_ret_slots() { let mut native = MockNative; let mut ctx = HostContext::new(None); // F0: CALL 1; HALT // F1: PUSH_I32 42; RET (expected 0 slots) let mut rom = Vec::new(); let f0_start = 0; rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 0, return_slots: 0, // ERROR: function pushes 42 but returns 0 ..Default::default() }, ]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_BAD_RET_SLOTS); assert_eq!(trap.opcode, OpCode::Ret as u16); assert!(trap.message.contains("Incorrect stack height")); } _ => panic!("Expected Trap(TRAP_BAD_RET_SLOTS), got {:?}", report.reason), } } #[test] fn test_locals_round_trip() { let mut native = MockNative; let mut ctx = HostContext::new(None); // PUSH_I32 42 // SET_LOCAL 0 // PUSH_I32 0 (garbage) // GET_LOCAL 0 // RET (1 slot) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&0i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Pop as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: 20, local_slots: 1, return_slots: 1, ..Default::default() }]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::EndOfRom); // RET pops return values and pushes them back on the caller stack (which is the sentinel frame's stack here). assert_eq!(vm.operand_stack, vec![Value::Int32(42)]); } #[test] fn test_locals_per_call_isolation() { let mut native = MockNative; let mut ctx = HostContext::new(None); // Function 0 (entry): // CALL 1 // POP // CALL 1 // HALT // Function 1: // GET_LOCAL 0 (should be Null initially) // PUSH_I32 42 // SET_LOCAL 0 // RET (1 slot: the initial Null) let mut rom = Vec::new(); // F0 let f0_start = 0; rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Pop as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; // F1 let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, local_slots: 1, return_slots: 1, ..Default::default() }, ]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); // The last value on stack is the return of the second CALL 1, // which should be Value::Null because locals are zero-initialized on each call. assert_eq!(vm.operand_stack.last().unwrap(), &Value::Null); } #[test] fn test_invalid_local_index_traps() { let mut native = MockNative; let mut ctx = HostContext::new(None); // Function with 0 params, 1 local. // GET_LOCAL 1 (OOB) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: 8, local_slots: 1, ..Default::default() }]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_INVALID_LOCAL); assert_eq!(trap.opcode, OpCode::GetLocal as u16); assert!(trap.message.contains("out of bounds")); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_nested_if() { let mut native = MockNative; let mut ctx = HostContext::new(None); // if (true) { // if (false) { // PUSH 1 // } else { // PUSH 2 // } // } else { // PUSH 3 // } // HALT let mut rom = Vec::new(); // 0: PUSH_BOOL true rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); rom.push(1); // 3: JMP_IF_FALSE -> ELSE1 (offset 42) rom.extend_from_slice(&(OpCode::JmpIfFalse as u16).to_le_bytes()); rom.extend_from_slice(&42u32.to_le_bytes()); // INNER IF: // 9: PUSH_BOOL false rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); rom.push(0); // 12: JMP_IF_FALSE -> ELSE2 (offset 30) rom.extend_from_slice(&(OpCode::JmpIfFalse as u16).to_le_bytes()); rom.extend_from_slice(&30u32.to_le_bytes()); // 18: PUSH_I32 1 rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&1i32.to_le_bytes()); // 24: JMP -> END (offset 48) rom.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); rom.extend_from_slice(&48u32.to_le_bytes()); // ELSE2: // 30: PUSH_I32 2 rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&2i32.to_le_bytes()); // 36: JMP -> END (offset 48) rom.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); rom.extend_from_slice(&48u32.to_le_bytes()); // ELSE1: // 42: PUSH_I32 3 rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&3i32.to_le_bytes()); // END: // 48: HALT rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); // We need to set up the function meta for absolute jumps to work correctly vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: 50, ..Default::default() }]); vm.prepare_call("0"); vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(vm.pop().unwrap(), Value::Int32(2)); } #[test] fn test_if_with_empty_branches() { let mut native = MockNative; let mut ctx = HostContext::new(None); // PUSH_BOOL true // JMP_IF_FALSE -> ELSE (offset 15) // // Empty then // JMP -> END (offset 15) // ELSE: // // Empty else // END: // HALT let mut rom = Vec::new(); // 0-2: PUSH_BOOL true rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); rom.push(1); // 3-8: JMP_IF_FALSE -> 15 rom.extend_from_slice(&(OpCode::JmpIfFalse as u16).to_le_bytes()); rom.extend_from_slice(&15u32.to_le_bytes()); // 9-14: JMP -> 15 rom.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); rom.extend_from_slice(&15u32.to_le_bytes()); // 15-16: HALT rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: 17, ..Default::default() }]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); assert_eq!(vm.operand_stack.len(), 0); } #[test] fn test_jmp_if_non_boolean_trap() { let mut native = MockNative; let mut ctx = HostContext::new(None); // PUSH_I32 1 // JMP_IF_TRUE 9 // HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&1i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::JmpIfTrue as u16).to_le_bytes()); rom.extend_from_slice(&9u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: 14, ..Default::default() }]); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(trap) => { assert_eq!(trap.code, TRAP_TYPE); assert_eq!(trap.opcode, OpCode::JmpIfTrue as u16); assert!(trap.message.contains("Expected boolean")); } _ => panic!("Expected Trap, got {:?}", report.reason), } } #[test] fn test_gc_triggers_only_at_frame_sync() { use crate::object::ObjectKind; // ROM: NOP; FRAME_SYNC; HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::Nop as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); // Set a very low threshold to trigger GC as soon as we hit FRAME_SYNC vm.gc_alloc_threshold = 1; // Allocate an unreachable object (no roots referencing it) let _orphan = vm.heap.allocate_object(ObjectKind::Bytes, &[1, 2, 3]); // +1 for the main coroutine allocated by new_test_vm assert_eq!(vm.heap.len(), 2); let mut native = MockNative; let mut ctx = HostContext::new(None); // Step 1: NOP — should not run GC vm.step(&mut native, &mut ctx).unwrap(); assert_eq!(vm.heap.len(), 2, "GC must not run except at safepoints"); // Step 2: FRAME_SYNC — GC should run and reclaim the unreachable object match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } // Main coroutine remains assert_eq!(vm.heap.len(), 1, "Unreachable object must be reclaimed at FRAME_SYNC"); } #[test] fn test_gc_keeps_roots_and_collects_unreachable_at_frame_sync() { use crate::object::ObjectKind; // ROM: FRAME_SYNC; FRAME_SYNC; HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); vm.gc_alloc_threshold = 1; // Allocate two objects; make one a root by placing it on the operand stack let rooted = vm.heap.allocate_object(ObjectKind::Bytes, &[9, 9]); let unreachable = vm.heap.allocate_object(ObjectKind::Bytes, &[8, 8, 8]); // +1 for main coroutine assert_eq!(vm.heap.len(), 3); vm.operand_stack.push(Value::HeapRef(rooted)); let mut native = MockNative; let mut ctx = HostContext::new(None); // Execute FRAME_SYNC: should trigger GC match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } // Rooted must survive; unreachable must be collected; main coroutine remains assert_eq!(vm.heap.len(), 2); assert!(vm.heap.is_valid(rooted)); assert!(!vm.heap.is_valid(unreachable)); } #[test] fn test_gc_simple_allocation_collection_cycle() { use crate::object::ObjectKind; // ROM: FRAME_SYNC; FRAME_SYNC; HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); // Make GC trigger on any allocation delta vm.gc_alloc_threshold = 1; // Cycle 1: allocate one unreachable object let _h1 = vm.heap.allocate_object(ObjectKind::Bytes, &[1]); // +1 for main coroutine assert_eq!(vm.heap.len(), 2); let mut native = MockNative; let mut ctx = HostContext::new(None); // FRAME_SYNC should collect it (first FRAME_SYNC) match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } // Main coroutine remains assert_eq!(vm.heap.len(), 1); // Cycle 2: allocate again and collect again deterministically let _h2 = vm.heap.allocate_object(ObjectKind::Bytes, &[2]); assert_eq!(vm.heap.len(), 2); // Second FRAME_SYNC should also be reached deterministically match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } assert_eq!(vm.heap.len(), 1); } #[test] fn test_gc_many_short_lived_objects_stress() { use crate::object::ObjectKind; // ROM: FRAME_SYNC; HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); // Deterministic: trigger collection when any growth since last sweep occurs vm.gc_alloc_threshold = 1; // Allocate many small, unreferenced objects let count = 2048usize; // stress but still quick for i in 0..count { let byte = (i & 0xFF) as u8; let _ = vm.heap.allocate_object(ObjectKind::Bytes, &[byte]); } // +1 for main coroutine assert_eq!(vm.heap.len(), count + 1); let mut native = MockNative; let mut ctx = HostContext::new(None); // Single FRAME_SYNC should reclaim all since there are no roots match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } assert_eq!(vm.heap.len(), 1, "All short-lived objects except main coroutine must be reclaimed deterministically"); } #[test] fn test_gc_keeps_objects_captured_by_suspended_coroutines() { use crate::object::ObjectKind; use crate::heap::CoroutineState; // ROM: FRAME_SYNC; HALT (trigger GC at safepoint) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); // Trigger GC at first FRAME_SYNC vm.gc_alloc_threshold = 1; // Allocate a heap object and a suspended coroutine that captures it on its stack let captured = vm.heap.allocate_object(ObjectKind::Bytes, &[0xAA, 0xBB]); let _coro = vm.heap.allocate_coroutine( 0, CoroutineState::Ready, 0, vec![Value::HeapRef(captured)], vec![], ); assert_eq!(vm.heap.len(), 3, "object + suspended coroutine + main coroutine must be allocated"); let mut native = MockNative; let mut ctx = HostContext::new(None); // FRAME_SYNC: GC runs and should keep both alive via suspended coroutine root match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } assert!(vm.heap.is_valid(captured), "captured object must remain alive"); // Captured object + suspended coroutine + main coroutine assert_eq!(vm.heap.len(), 3, "both coroutine and captured object must survive (plus main)"); } #[test] fn test_gc_collects_finished_coroutine() { use crate::heap::CoroutineState; // ROM: FRAME_SYNC; HALT (trigger GC at safepoint) let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); vm.gc_alloc_threshold = 1; // Allocate a finished coroutine with no external references let finished = vm.heap.allocate_coroutine(0, CoroutineState::Finished, 0, vec![], vec![]); assert!(vm.heap.is_valid(finished)); let mut native = MockNative; let mut ctx = HostContext::new(None); // FRAME_SYNC: GC should collect the finished coroutine since it's not a root match vm.step(&mut native, &mut ctx) { Err(LogicalFrameEndingReason::FrameSync) => {} other => panic!("Expected FrameSync, got {:?}", other), } assert!(!vm.heap.is_valid(finished), "finished coroutine must be collected"); // Main coroutine remains allocated assert_eq!(vm.heap.len(), 1, "only main coroutine should remain"); } #[test] fn test_coroutines_strict_alternation_with_yield() { use prometeu_bytecode::FunctionMeta; // Build function A: PUSH 1; YIELD; FRAME_SYNC; JMP 0 (loop) let mut fn_a = Vec::new(); fn_a.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); fn_a.extend_from_slice(&1i32.to_le_bytes()); fn_a.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); fn_a.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_a.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); fn_a.extend_from_slice(&0u32.to_le_bytes()); // Build function B: PUSH 2; YIELD; FRAME_SYNC; JMP 0 (loop) let mut fn_b = Vec::new(); fn_b.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); fn_b.extend_from_slice(&2i32.to_le_bytes()); fn_b.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); fn_b.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_b.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); fn_b.extend_from_slice(&0u32.to_le_bytes()); // Main: SPAWN A; SPAWN B; SLEEP 100; HALT let mut main = Vec::new(); main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&1u32.to_le_bytes()); // fn A idx main.extend_from_slice(&0u32.to_le_bytes()); // arg count main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&2u32.to_le_bytes()); // fn B idx main.extend_from_slice(&0u32.to_le_bytes()); // arg count main.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); main.extend_from_slice(&100u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); // Assemble ROM: [main][A][B] let off_main = 0usize; let off_a = main.len(); let off_b = off_a + fn_a.len(); let mut rom = Vec::with_capacity(main.len() + fn_a.len() + fn_b.len()); rom.extend_from_slice(&main); rom.extend_from_slice(&fn_a); rom.extend_from_slice(&fn_b); // VM with three functions (0=main, 1=A, 2=B) let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, FunctionMeta { code_offset: off_a as u32, code_len: fn_a.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, FunctionMeta { code_offset: off_b as u32, code_len: fn_b.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, ]); let mut native = MockNative; let mut ctx = HostContext::new(None); // Frame 1: main sleeps; from now on A and B should strictly alternate. let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap(); // Locate coroutine handles for A (fn_idx=1) and B (fn_idx=2) let mut a_href = None; let mut b_href = None; // Consider currently running coroutine if let Some(cur) = vm.current_coro { if let Some(f) = vm.call_stack.last() { if f.func_idx == 1 { a_href = Some(cur); } if f.func_idx == 2 { b_href = Some(cur); } } } // And also consider suspended (Ready/Sleeping) coroutines for h in vm.heap.suspended_coroutine_handles() { if let Some(co) = vm.heap.coroutine_data(h) { if let Some(f) = co.frames.last() { if f.func_idx == 1 { a_href = Some(h); } if f.func_idx == 2 { b_href = Some(h); } } } } let a_href = a_href.expect("coroutine A not found"); let b_href = b_href.expect("coroutine B not found"); let mut prev_a = vm.heap.coroutine_data(a_href).unwrap().stack.len(); let mut prev_b = vm.heap.coroutine_data(b_href).unwrap().stack.len(); let mut trace = Vec::new(); for _ in 0..6 { let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap(); let a_now = vm.heap.coroutine_data(a_href).unwrap().stack.len(); let b_now = vm.heap.coroutine_data(b_href).unwrap().stack.len(); if a_now > prev_a { trace.push(1); } else if b_now > prev_b { trace.push(2); } else { panic!("no coroutine progress detected this frame"); } prev_a = a_now; prev_b = b_now; } assert_eq!(trace, vec![1, 2, 1, 2, 1, 2], "Coroutines must strictly alternate under Yield"); } #[test] fn test_sleep_does_not_stall_others_and_wakes_at_exact_tick() { use prometeu_bytecode::FunctionMeta; // Function A: SLEEP N; PUSH 100; YIELD; FRAME_SYNC; HALT let sleep_n: u32 = 3; let mut fn_a = Vec::new(); fn_a.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); fn_a.extend_from_slice(&sleep_n.to_le_bytes()); fn_a.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); fn_a.extend_from_slice(&100i32.to_le_bytes()); fn_a.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); fn_a.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_a.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); // Function B: PUSH 1; YIELD; FRAME_SYNC; JMP 0 (increments every frame) let mut fn_b = Vec::new(); fn_b.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); fn_b.extend_from_slice(&1i32.to_le_bytes()); fn_b.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); fn_b.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_b.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); fn_b.extend_from_slice(&0u32.to_le_bytes()); // Main: SPAWN A; SPAWN B; SLEEP big; HALT let mut main = Vec::new(); main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&1u32.to_le_bytes()); main.extend_from_slice(&0u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&2u32.to_le_bytes()); main.extend_from_slice(&0u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); main.extend_from_slice(&100u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let off_main = 0usize; let off_a = main.len(); let off_b = off_a + fn_a.len(); let mut rom = Vec::new(); rom.extend_from_slice(&main); rom.extend_from_slice(&fn_a); rom.extend_from_slice(&fn_b); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, FunctionMeta { code_offset: off_a as u32, code_len: fn_a.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, FunctionMeta { code_offset: off_b as u32, code_len: fn_b.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, ]); let mut native = MockNative; let mut ctx = HostContext::new(None); // Frame 1: main sleeps, tick -> 1 let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(vm.current_tick, 1); // Identify A and B coroutine handles (consider both running and suspended) let mut a_href = None; let mut b_href = None; if let Some(cur) = vm.current_coro { if let Some(f) = vm.call_stack.last() { if f.func_idx == 1 { a_href = Some(cur); } if f.func_idx == 2 { b_href = Some(cur); } } } for h in vm.heap.suspended_coroutine_handles() { if let Some(co) = vm.heap.coroutine_data(h) { if let Some(f) = co.frames.last() { if f.func_idx == 1 { a_href = Some(h); } if f.func_idx == 2 { b_href = Some(h); } } } } let a_href = a_href.expect("A not found"); let b_href = b_href.expect("B not found"); // Count how many frames B runs while A sleeps using the scheduler's next-to-run handle. // Stop when A is scheduled to run, then execute that frame and record its end-of-frame tick. let mut ones_before = 0usize; let mut woke_at_tick = 0u64; let mut seen_a_once = false; for _ in 0..1000 { if let Some(next) = vm.scheduler.current() { if next == a_href { if seen_a_once { // A has slept before and is about to run again (wake). Run and record. let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap(); woke_at_tick = vm.current_tick; break; } else { // First time A runs (to execute SLEEP N); do not count as wake yet. seen_a_once = true; } } else if next == b_href { ones_before += 1; } } let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap(); } // Canonical semantics: wake_tick = current_tick_at_sleep + N + 1. // The scheduler wakes sleepers at the end of that tick, so the coroutine runs // in the following frame, and we observe its heap stack update at end tick = wake_tick + 1. // A executes SLEEP at its first run (tick 1), so wake_tick = 1 + N + 1, observed tick = +1. let expected_observed_end_tick = 1u64 + sleep_n as u64 + 2u64; assert_eq!(woke_at_tick, expected_observed_end_tick, "A must wake at the exact tick (+1 frame to observe)"); // And B must have produced at least N items (one per frame) before A's wake. assert!(ones_before as u64 >= sleep_n as u64, "B must keep running while A sleeps"); } #[test] fn test_multi_coroutine_determinism_across_runs() { use prometeu_bytecode::FunctionMeta; // Reuse alternation program from previous test let mut fn_a = Vec::new(); fn_a.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); fn_a.extend_from_slice(&1i32.to_le_bytes()); fn_a.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); fn_a.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_a.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); fn_a.extend_from_slice(&0u32.to_le_bytes()); let mut fn_b = Vec::new(); fn_b.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); fn_b.extend_from_slice(&2i32.to_le_bytes()); fn_b.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes()); fn_b.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_b.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); fn_b.extend_from_slice(&0u32.to_le_bytes()); let mut main = Vec::new(); main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&1u32.to_le_bytes()); main.extend_from_slice(&0u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&2u32.to_le_bytes()); main.extend_from_slice(&0u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); main.extend_from_slice(&100u32.to_le_bytes()); main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let off_main = 0usize; let off_a = main.len(); let off_b = off_a + fn_a.len(); let mut rom = Vec::new(); rom.extend_from_slice(&main); rom.extend_from_slice(&fn_a); rom.extend_from_slice(&fn_b); let mut vm1 = new_test_vm(rom.clone(), vec![]); let mut vm2 = new_test_vm(rom.clone(), vec![]); let fm: std::sync::Arc<[prometeu_bytecode::FunctionMeta]> = std::sync::Arc::from(vec![ FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, FunctionMeta { code_offset: off_a as u32, code_len: fn_a.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, FunctionMeta { code_offset: off_b as u32, code_len: fn_b.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, ]); vm1.program.functions = fm.clone(); vm2.program.functions = fm; let mut native = MockNative; let mut c1 = HostContext::new(None); let mut c2 = HostContext::new(None); // Burn first frame (main sleeps) let _ = vm1.run_budget(100, &mut native, &mut c1).unwrap(); let _ = vm2.run_budget(100, &mut native, &mut c2).unwrap(); // Discover A/B handles in both VMs let find_ab = |vm: &VirtualMachine| { let mut a = None; let mut b = None; // running if let Some(cur) = vm.current_coro { if let Some(f) = vm.call_stack.last() { if f.func_idx == 1 { a = Some(cur); } if f.func_idx == 2 { b = Some(cur); } } } // suspended for h in vm.heap.suspended_coroutine_handles() { if let Some(co) = vm.heap.coroutine_data(h) { if let Some(f) = co.frames.last() { if f.func_idx == 1 { a = Some(h); } if f.func_idx == 2 { b = Some(h); } } } } (a.expect("A missing"), b.expect("B missing")) }; let (a1, b1) = find_ab(&vm1); let (a2, b2) = find_ab(&vm2); let mut a1_prev = vm1.heap.coroutine_data(a1).unwrap().stack.len(); let mut b1_prev = vm1.heap.coroutine_data(b1).unwrap().stack.len(); let mut a2_prev = vm2.heap.coroutine_data(a2).unwrap().stack.len(); let mut b2_prev = vm2.heap.coroutine_data(b2).unwrap().stack.len(); let mut trace1 = Vec::new(); let mut trace2 = Vec::new(); for _ in 0..8 { let _ = vm1.run_budget(100, &mut native, &mut c1).unwrap(); let a_now = vm1.heap.coroutine_data(a1).unwrap().stack.len(); let b_now = vm1.heap.coroutine_data(b1).unwrap().stack.len(); if a_now > a1_prev { trace1.push(1); } else if b_now > b1_prev { trace1.push(2); } else { panic!("no progress 1"); } a1_prev = a_now; b1_prev = b_now; let _ = vm2.run_budget(100, &mut native, &mut c2).unwrap(); let a2_now = vm2.heap.coroutine_data(a2).unwrap().stack.len(); let b2_now = vm2.heap.coroutine_data(b2).unwrap().stack.len(); if a2_now > a2_prev { trace2.push(1); } else if b2_now > b2_prev { trace2.push(2); } else { panic!("no progress 2"); } a2_prev = a2_now; b2_prev = b2_now; } assert_eq!(trace1, trace2, "Execution trace (coroutine IDs) must match exactly across runs"); } #[test] fn test_gc_with_suspended_coroutine_runtime() { use crate::object::ObjectKind; use prometeu_bytecode::FunctionMeta; // Function F (idx 1): SLEEP 10; FRAME_SYNC; HALT let mut fn_f = Vec::new(); fn_f.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes()); fn_f.extend_from_slice(&10u32.to_le_bytes()); fn_f.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); fn_f.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); // Main (idx 0): SPAWN F with 1 argument (the HeapRef we preload); FRAME_SYNC; HALT let mut main = Vec::new(); main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes()); main.extend_from_slice(&1u32.to_le_bytes()); // func idx main.extend_from_slice(&1u32.to_le_bytes()); // arg count = 1 main.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes()); main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let off_main = 0usize; let off_f = main.len(); let mut rom = Vec::new(); rom.extend_from_slice(&main); rom.extend_from_slice(&fn_f); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, // Function F takes 1 parameter (the HeapRef) which stays on its stack while sleeping FunctionMeta { code_offset: off_f as u32, code_len: fn_f.len() as u32, param_slots: 1, local_slots: 0, return_slots: 0, max_stack_slots: 8 }, ]); // Force GC at first safepoint to stress retention vm.gc_alloc_threshold = 1; // Allocate a heap object and preload it onto main's operand stack so SPAWN consumes it as arg. let captured = vm.heap.allocate_object(ObjectKind::Bytes, &[0xAB, 0xCD, 0xEF]); vm.operand_stack.push(Value::HeapRef(captured)); let mut native = MockNative; let mut ctx = HostContext::new(None); // Run main: SPAWN consumes the HeapRef as arg and creates sleeping coroutine; FRAME_SYNC triggers GC let rep = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert!(matches!(rep.reason, LogicalFrameEndingReason::FrameSync)); // The captured object must remain alive because it is referenced by the sleeping coroutine's stack assert!(vm.heap.is_valid(captured), "captured object must remain alive while coroutine sleeps"); } #[test] fn test_make_closure_zero_captures() { use prometeu_bytecode::{FunctionMeta, Value}; // ROM: MAKE_CLOSURE fn_id=7, cap=0; HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes()); rom.extend_from_slice(&7u32.to_le_bytes()); // fn_id rom.extend_from_slice(&0u32.to_le_bytes()); // capture_count rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); let mut native = MockNative; let mut ctx = HostContext::new(None); // step MAKE_CLOSURE vm.step(&mut native, &mut ctx).unwrap(); // step HALT vm.step(&mut native, &mut ctx).unwrap(); assert!(vm.halted); assert_eq!(vm.operand_stack.len(), 1); let top = vm.peek().unwrap().clone(); let href = match top { Value::HeapRef(h) => h, _ => panic!("Expected HeapRef on stack") }; assert!(vm.heap.is_valid(href)); assert_eq!(vm.heap.closure_fn_id(href), Some(7)); let env = vm.heap.closure_env_slice(href).expect("env slice"); assert_eq!(env.len(), 0); } #[test] fn test_make_closure_multiple_captures_and_order() { use prometeu_bytecode::{FunctionMeta, Value}; // Build ROM: // PUSH_I32 1; PUSH_I32 2; PUSH_I32 3; // Stack: [1,2,3] // MAKE_CLOSURE fn_id=9, cap=3; // Pops 3 (3,2,1), env = [1,2,3] // HALT let mut rom = Vec::new(); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&1i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&2i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&3i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes()); rom.extend_from_slice(&9u32.to_le_bytes()); // fn_id rom.extend_from_slice(&3u32.to_le_bytes()); // capture_count rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }]); let mut native = MockNative; let mut ctx = HostContext::new(None); // Execute instructions until HALT while !vm.halted { vm.step(&mut native, &mut ctx).unwrap(); } // After HALT, stack must contain only the closure ref assert_eq!(vm.operand_stack.len(), 1); let href = match vm.pop().unwrap() { Value::HeapRef(h) => h, _ => panic!("Expected HeapRef") }; assert_eq!(vm.heap.closure_fn_id(href), Some(9)); let env = vm.heap.closure_env_slice(href).expect("env slice"); assert_eq!(env.len(), 3); assert_eq!(env[0], Value::Int32(1)); assert_eq!(env[1], Value::Int32(2)); assert_eq!(env[2], Value::Int32(3)); } #[test] fn test_call_closure_returns_constant() { use prometeu_bytecode::{FunctionMeta, Value}; // F0 (entry): MAKE_CLOSURE fn=1, cap=0; CALL_CLOSURE argc=0; HALT // F1 (callee): PUSH_I32 7; RET let mut rom = Vec::new(); let f0_start = 0usize; rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); // fn_id rom.extend_from_slice(&0u32.to_le_bytes()); // capture_count rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); // argc = 0 user args rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; // F1 code let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&7i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() }, ]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); assert_eq!(vm.operand_stack.len(), 1); assert_eq!(vm.operand_stack[0], Value::Int32(7)); } #[test] fn test_call_closure_with_captures_ignored() { use prometeu_bytecode::{FunctionMeta, Value}; // F0: PUSH_I32 123; MAKE_CLOSURE fn=1 cap=1; CALL_CLOSURE 0; HALT // F1: PUSH_I32 42; RET let mut rom = Vec::new(); let f0_start = 0usize; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&123i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); // fn_id rom.extend_from_slice(&1u32.to_le_bytes()); // capture_count rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); // argc = 0 rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&42i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f1_len = rom.len() as u32 - f1_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() }, ]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); assert_eq!(vm.operand_stack, vec![Value::Int32(42)]); } #[test] fn test_call_closure_on_non_closure_traps() { use prometeu_bytecode::FunctionMeta; // F0: PUSH_I32 1; CALL_CLOSURE 0; HALT -> should TRAP_TYPE on CALL_CLOSURE let mut rom = Vec::new(); let f0_start = 0usize; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&1i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); // Leave HALT for after run to ensure we trap before rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(10, &mut native, &mut ctx).unwrap(); match report.reason { LogicalFrameEndingReason::Trap(info) => { assert_eq!(info.code, TRAP_TYPE); assert_eq!(info.opcode, OpCode::CallClosure as u16); } other => panic!("Expected Trap(TYPE) from CALL_CLOSURE on non-closure, got {:?}", other), } } #[test] fn test_nested_call_closure() { use prometeu_bytecode::{FunctionMeta, Value}; // F0: MAKE_CLOSURE fn=1 cap=0; CALL_CLOSURE 0; CALL_CLOSURE 0; HALT // F1: MAKE_CLOSURE fn=2 cap=0; RET // returns a closure // F2: PUSH_I32 55; RET // returns constant let mut rom = Vec::new(); // F0 let f0_start = 0usize; rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes()); rom.extend_from_slice(&1u32.to_le_bytes()); // fn_id = 1 rom.extend_from_slice(&0u32.to_le_bytes()); // cap=0 rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); // argc=0 -> pushes a closure from F1 rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); // argc=0 -> call returned closure F2 rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let f0_len = rom.len() - f0_start; // F1 let f1_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes()); rom.extend_from_slice(&2u32.to_le_bytes()); // fn_id = 2 rom.extend_from_slice(&0u32.to_le_bytes()); // cap=0 rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); // return the HeapRef on stack let f1_len = rom.len() as u32 - f1_start; // F2 let f2_start = rom.len() as u32; rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&55i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let f2_len = rom.len() as u32 - f2_start; let mut vm = new_test_vm(rom.clone(), vec![]); vm.program.functions = std::sync::Arc::from(vec![ FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }, FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() }, FunctionMeta { code_offset: f2_start, code_len: f2_len, param_slots: 1, return_slots: 1, ..Default::default() }, ]); let mut native = MockNative; let mut ctx = HostContext::new(None); vm.prepare_call("0"); let report = vm.run_budget(200, &mut native, &mut ctx).unwrap(); assert_eq!(report.reason, LogicalFrameEndingReason::Halted); assert_eq!(vm.operand_stack, vec![Value::Int32(55)]); } }