diff --git a/Cargo.lock b/Cargo.lock index 643bb2cf..ada203b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1630,6 +1630,7 @@ dependencies = [ name = "prometeu-firmware" version = "0.1.0" dependencies = [ + "prometeu-bytecode", "prometeu-drivers", "prometeu-hal", "prometeu-system", diff --git a/crates/console/prometeu-bytecode/src/abi.rs b/crates/console/prometeu-bytecode/src/abi.rs index 311530af..9cd65b37 100644 --- a/crates/console/prometeu-bytecode/src/abi.rs +++ b/crates/console/prometeu-bytecode/src/abi.rs @@ -3,6 +3,8 @@ /// Attempted to execute an unknown or invalid opcode. pub const TRAP_ILLEGAL_INSTRUCTION: u32 = 0x0000_0001; +/// Program explicitly requested termination via the TRAP opcode. +pub const TRAP_EXPLICIT: u32 = 0x0000_0002; /// Out-of-bounds access (e.g., stack/heap/local index out of range). pub const TRAP_OOB: u32 = 0x0000_0003; /// Type mismatch for the attempted operation (e.g., wrong operand type or syscall argument type). diff --git a/crates/console/prometeu-bytecode/src/lib.rs b/crates/console/prometeu-bytecode/src/lib.rs index fce85565..d56276df 100644 --- a/crates/console/prometeu-bytecode/src/lib.rs +++ b/crates/console/prometeu-bytecode/src/lib.rs @@ -11,8 +11,8 @@ mod program_image; mod value; pub use abi::{ - TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC, - TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, + TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_EXPLICIT, TRAP_ILLEGAL_INSTRUCTION, + TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, }; pub use assembler::{assemble, AsmError}; diff --git a/crates/console/prometeu-firmware/Cargo.toml b/crates/console/prometeu-firmware/Cargo.toml index 37292655..4ec395cc 100644 --- a/crates/console/prometeu-firmware/Cargo.toml +++ b/crates/console/prometeu-firmware/Cargo.toml @@ -11,3 +11,4 @@ prometeu-hal = { path = "../prometeu-hal" } [dev-dependencies] prometeu-drivers = { path = "../prometeu-drivers" } +prometeu-bytecode = { path = "../prometeu-bytecode" } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 8032e022..8d33e88f 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -168,8 +168,12 @@ impl Firmware { #[cfg(test)] mod tests { use super::*; + use prometeu_bytecode::assembler::assemble; + use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; use prometeu_hal::cartridge::AppMode; + use prometeu_hal::syscalls::caps; + use prometeu_system::CrashReport; fn invalid_game_cartridge() -> Cartridge { Cartridge { @@ -186,6 +190,43 @@ mod tests { } } + fn trapping_game_cartridge() -> Cartridge { + let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble"); + let program = BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![FunctionMeta { + code_offset: 0, + code_len: code.len() as u32, + ..Default::default() + }], + code, + debug_info: None, + exports: vec![], + syscalls: vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + } + .serialize(); + + Cartridge { + app_id: 8, + title: "Trap Cart".into(), + app_version: "1.0.0".into(), + app_mode: AppMode::Game, + entrypoint: "".into(), + capabilities: caps::GFX, + program, + assets: vec![], + asset_table: vec![], + preload: vec![], + } + } + #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); @@ -196,9 +237,36 @@ mod tests { firmware.tick(&signals, &mut hardware); match &firmware.state { - FirmwareState::AppCrashes(step) => { - assert!(step.error.contains("InvalidFormat")); - } + FirmwareState::AppCrashes(step) => match &step.report { + CrashReport::VmInit { error } => { + assert!(matches!(error, prometeu_vm::VmInitError::InvalidFormat)); + } + other => panic!("expected VmInit crash report, got {:?}", other), + }, + other => panic!("expected AppCrashes state, got {:?}", other), + } + } + + #[test] + fn game_running_transitions_to_app_crashes_when_runtime_surfaces_trap() { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(trapping_game_cartridge()); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + + firmware.tick(&signals, &mut hardware); + + match &firmware.state { + FirmwareState::AppCrashes(step) => match &step.report { + CrashReport::VmTrap { trap } => { + assert!(trap.message.contains("Expected integer at index 0")); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + }, other => panic!("expected AppCrashes state, got {:?}", other), } } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs index 882164d8..8133a3cc 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs @@ -2,15 +2,20 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; +use prometeu_system::CrashReport; #[derive(Debug, Clone)] pub struct AppCrashesStep { - pub error: String, + pub report: CrashReport, } impl AppCrashesStep { + pub fn log_message(&self) -> String { + format!("App Crashed: {}", self.report) + } + pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { - ctx.os.log(LogLevel::Error, LogSource::Pos, 0, format!("App Crashed: {}", self.error)); + ctx.os.log(LogLevel::Error, LogSource::Pos, self.report.log_tag(), self.log_message()); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { @@ -33,3 +38,30 @@ impl AppCrashesStep { pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {} } + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_bytecode::TrapInfo; + + #[test] + fn crash_step_formats_trap_from_structured_report() { + let step = AppCrashesStep { + report: CrashReport::VmTrap { + trap: TrapInfo { + code: 0xAB, + opcode: 0x22, + message: "type mismatch".into(), + pc: 0x44, + span: None, + }, + }, + }; + + let msg = step.log_message(); + assert!(msg.contains("PVM Trap 0x000000AB")); + assert!(msg.contains("PC 0x44")); + assert!(msg.contains("opcode 0x0022")); + assert!(msg.contains("type mismatch")); + } +} diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 8bdbd737..9ed3376f 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -17,7 +17,7 @@ impl GameRunningStep { ctx.hw.gfx_mut().present(); } - result.map(|err| FirmwareState::AppCrashes(AppCrashesStep { error: err })) + result.map(|report| FirmwareState::AppCrashes(AppCrashesStep { report })) } pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {} diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs index 55acf96f..f3c8b348 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs @@ -30,8 +30,8 @@ impl HubHomeStep { ctx.hw.gfx_mut().present(); - if let Some(err) = error { - return Some(FirmwareState::AppCrashes(AppCrashesStep { error: err })); + if let Some(report) = error { + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } None diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs index 2e75a322..ea6e937f 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs @@ -6,11 +6,12 @@ use prometeu_hal::cartridge::{AppMode, Cartridge}; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::window::Rect; +use prometeu_system::CrashReport; #[derive(Debug, Clone)] pub struct LoadCartridgeStep { pub cartridge: Cartridge, - init_error: Option, + init_error: Option, } impl LoadCartridgeStep { @@ -33,13 +34,12 @@ impl LoadCartridgeStep { self.cartridge.assets.clone(), ); - self.init_error = - ctx.os.initialize_vm(ctx.vm, &self.cartridge).err().map(|err| format!("{:?}", err)); + self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err(); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { - if let Some(error) = self.init_error.take() { - return Some(FirmwareState::AppCrashes(AppCrashesStep { error })); + if let Some(report) = self.init_error.take() { + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } if self.cartridge.app_mode == AppMode::System { diff --git a/crates/console/prometeu-hal/src/debugger_protocol.rs b/crates/console/prometeu-hal/src/debugger_protocol.rs index acb929b0..8c6d9999 100644 --- a/crates/console/prometeu-hal/src/debugger_protocol.rs +++ b/crates/console/prometeu-hal/src/debugger_protocol.rs @@ -71,6 +71,14 @@ pub enum DebugEvent { audio_inflight_bytes: usize, audio_slots_occupied: u32, }, + #[serde(rename = "fault")] + Fault { + kind: String, + summary: String, + pc: Option, + trap_code: Option, + opcode: Option, + }, #[serde(rename = "cert")] Cert { rule: String, used: u64, limit: u64, frame_index: u64 }, } @@ -119,4 +127,21 @@ mod tests { assert!(json.contains("\"frame_index\":5")); assert!(json.contains("\"app_id\":1")); } + + #[test] + fn test_fault_event_serialization() { + let event = DebugEvent::Fault { + kind: "vm_trap".into(), + summary: "PVM Trap 0x00000004".into(), + pc: Some(12), + trap_code: Some(4), + opcode: Some(0x70), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"fault\"")); + assert!(json.contains("\"kind\":\"vm_trap\"")); + assert!(json.contains("\"trap_code\":4")); + assert!(json.contains("\"opcode\":112")); + } } diff --git a/crates/console/prometeu-system/src/crash_report.rs b/crates/console/prometeu-system/src/crash_report.rs new file mode 100644 index 00000000..72838665 --- /dev/null +++ b/crates/console/prometeu-system/src/crash_report.rs @@ -0,0 +1,55 @@ +use prometeu_bytecode::TrapInfo; +use prometeu_vm::VmInitError; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CrashReport { + VmTrap { trap: TrapInfo }, + VmPanic { message: String, pc: Option }, + VmInit { error: VmInitError }, +} + +impl CrashReport { + pub fn kind(&self) -> &'static str { + match self { + Self::VmTrap { .. } => "vm_trap", + Self::VmPanic { .. } => "vm_panic", + Self::VmInit { .. } => "vm_init", + } + } + + pub fn pc(&self) -> Option { + match self { + Self::VmTrap { trap } => Some(trap.pc), + Self::VmPanic { pc, .. } => *pc, + Self::VmInit { .. } => None, + } + } + + pub fn summary(&self) -> String { + match self { + Self::VmTrap { trap } => format!( + "PVM Trap 0x{:08X} at PC 0x{:X} (opcode 0x{:04X}): {}", + trap.code, trap.pc, trap.opcode, trap.message + ), + Self::VmPanic { message, pc: Some(pc) } => { + format!("PVM Panic at PC 0x{:X}: {}", pc, message) + } + Self::VmPanic { message, pc: None } => format!("PVM Panic: {}", message), + Self::VmInit { error } => format!("PVM Init Error: {:?}", error), + } + } + + pub fn log_tag(&self) -> u16 { + match self { + Self::VmTrap { trap } => trap.code as u16, + Self::VmPanic { .. } | Self::VmInit { .. } => 0, + } + } +} + +impl fmt::Display for CrashReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.summary()) + } +} diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 1c151f13..53dc009c 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -1,7 +1,9 @@ +mod crash_report; mod programs; mod services; mod virtual_machine_runtime; +pub use crash_report::CrashReport; pub use programs::PrometeuHub; pub use services::fs; pub use virtual_machine_runtime::VirtualMachineRuntime; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index e34677fc..81d5a0ae 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -1,9 +1,9 @@ -use crate::VirtualMachineRuntime; use crate::programs::prometeu_hub::window_manager::WindowManager; -use prometeu_hal::HardwareBridge; +use crate::VirtualMachineRuntime; use prometeu_hal::color::Color; use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::window::Rect; +use prometeu_hal::HardwareBridge; /// PrometeuHub: Launcher and system UI environment. pub struct PrometeuHub { diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime.rs b/crates/console/prometeu-system/src/virtual_machine_runtime.rs index 4f891a9a..86586d27 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime.rs @@ -1,5 +1,6 @@ use crate::fs::{FsBackend, FsState, VirtualFS}; -use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; +use crate::CrashReport; +use prometeu_bytecode::{Value, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE}; use prometeu_hal::asset::{BankType, LoadStatus, SlotRef}; use prometeu_hal::button::Button; use prometeu_hal::cartridge::{AppMode, Cartridge}; @@ -10,9 +11,9 @@ use prometeu_hal::syscalls::Syscall; use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame}; use prometeu_hal::tile::Tile; use prometeu_hal::vm_fault::VmFault; +use prometeu_hal::{expect_bool, expect_int, HostContext, HostReturn, NativeInterface, SyscallId}; use prometeu_hal::{HardwareBridge, InputSignals}; -use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; -use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine, VmInitError}; +use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine}; use std::collections::HashMap; use std::time::Instant; @@ -55,6 +56,8 @@ pub struct VirtualMachineRuntime { pub telemetry_current: TelemetryFrame, /// The results of the last successfully completed logical frame. pub telemetry_last: TelemetryFrame, + /// Last terminal crash report surfaced by the runtime, if any. + pub last_crash_report: Option, /// Logic for validating that the app obeys the console's Certification (CAP). pub certifier: Certifier, /// Pause state: When true, `tick()` will not advance the VM. @@ -104,6 +107,7 @@ impl VirtualMachineRuntime { logs_written_this_frame: HashMap::new(), telemetry_current: TelemetryFrame::default(), telemetry_last: TelemetryFrame::default(), + last_crash_report: None, certifier: Certifier::new(cap_config.unwrap_or_default()), paused: false, debug_step_request: false, @@ -161,13 +165,34 @@ impl VirtualMachineRuntime { } } - pub fn reset(&mut self, vm: &mut VirtualMachine) { - *vm = VirtualMachine::default(); - self.tick_index = 0; + fn clear_cartridge_state(&mut self) { self.logical_frame_index = 0; self.logical_frame_active = false; self.logical_frame_remaining_cycles = 0; self.last_frame_cpu_time_us = 0; + + self.open_files.clear(); + self.next_handle = 1; + + self.current_app_id = 0; + self.current_cartridge_title.clear(); + self.current_cartridge_app_version.clear(); + self.current_cartridge_app_mode = AppMode::Game; + self.current_entrypoint.clear(); + self.logs_written_this_frame.clear(); + + self.telemetry_current = TelemetryFrame::default(); + self.telemetry_last = TelemetryFrame::default(); + self.last_crash_report = None; + + self.paused = false; + self.debug_step_request = false; + self.needs_prepare_entry_call = false; + } + + pub fn reset(&mut self, vm: &mut VirtualMachine) { + *vm = VirtualMachine::default(); + self.clear_cartridge_state(); } /// Loads a cartridge into the PVM and resets the execution state. @@ -175,7 +200,8 @@ impl VirtualMachineRuntime { &mut self, vm: &mut VirtualMachine, cartridge: &Cartridge, - ) -> Result<(), VmInitError> { + ) -> Result<(), CrashReport> { + self.clear_cartridge_state(); vm.set_capabilities(cartridge.capabilities); match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) { @@ -189,15 +215,17 @@ impl VirtualMachineRuntime { Ok(()) } Err(e) => { + let report = CrashReport::VmInit { error: e }; + self.last_crash_report = Some(report.clone()); self.log( LogLevel::Error, LogSource::Vm, - 0, - format!("Failed to initialize VM: {:?}", e), + report.log_tag(), + format!("Failed to initialize VM: {}", report), ); // Fail fast: no program is installed, no app id is switched. // We don't update current_app_id or other fields. - Err(e) + Err(report) } } } @@ -207,14 +235,29 @@ impl VirtualMachineRuntime { &mut self, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge, - ) -> Option { + ) -> Option { let mut ctx = HostContext::new(Some(hw)); match vm.step(self, &mut ctx) { Ok(_) => None, Err(e) => { - let err_msg = format!("PVM Fault during Step: {:?}", e); - self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); - Some(err_msg) + let report = match e { + LogicalFrameEndingReason::Trap(trap) => CrashReport::VmTrap { trap }, + LogicalFrameEndingReason::Panic(message) => { + CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) } + } + other => CrashReport::VmPanic { + message: format!("Unexpected fault during step: {:?}", other), + pc: Some(vm.pc() as u32), + }, + }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + format!("PVM Fault during Step: {}", report), + ); + self.last_crash_report = Some(report.clone()); + Some(report) } } } @@ -229,7 +272,7 @@ impl VirtualMachineRuntime { vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge, - ) -> Option { + ) -> Option { let start = Instant::now(); self.tick_index += 1; @@ -303,9 +346,28 @@ impl VirtualMachineRuntime { // Handle Panics if let LogicalFrameEndingReason::Panic(err) = run.reason { - let err_msg = format!("PVM Fault: \"{}\"", err); - self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); - return Some(err_msg); + let report = + CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + report.summary(), + ); + self.last_crash_report = Some(report.clone()); + return Some(report); + } + + if let LogicalFrameEndingReason::Trap(trap) = &run.reason { + let report = CrashReport::VmTrap { trap: trap.clone() }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + report.summary(), + ); + self.last_crash_report = Some(report.clone()); + return Some(report); } // 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned) @@ -351,9 +413,10 @@ impl VirtualMachineRuntime { } Err(e) => { // Fatal VM fault (division by zero, invalid memory access, etc). - let err_msg = format!("PVM Fault: {:?}", e); - self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); - return Some(err_msg); + let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) }; + self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary()); + self.last_crash_report = Some(report.clone()); + return Some(report); } } } @@ -1129,8 +1192,12 @@ mod tests { use super::*; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; + use prometeu_bytecode::TRAP_TYPE; + use prometeu_drivers::hardware::Hardware; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::syscalls::caps; + use prometeu_hal::InputSignals; + use prometeu_vm::VmInitError; fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { Cartridge { @@ -1183,7 +1250,10 @@ mod tests { let res = runtime.initialize_vm(&mut vm, &cartridge); - assert!(res.is_err()); + assert!(matches!( + res, + Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }) + )); assert_eq!(runtime.current_app_id, 0); assert_eq!(vm.pc(), 0); assert_eq!(vm.operand_stack_top(1), Vec::::new()); @@ -1212,4 +1282,196 @@ mod tests { assert_eq!(runtime.current_app_id, 42); assert!(!vm.is_halted()); } + + #[test] + fn tick_returns_error_when_vm_ends_slice_with_trap() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + + let report = runtime + .tick(&mut vm, &signals, &mut hardware) + .expect("trap must surface as runtime error"); + + match report { + CrashReport::VmTrap { trap } => { + assert_eq!(trap.code, TRAP_TYPE); + assert!(trap.message.contains("Expected integer at index 0")); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + } + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); + } + + #[test] + fn tick_returns_panic_report_distinct_from_trap() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); + + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + let report = runtime + .tick(&mut vm, &signals, &mut hardware) + .expect("panic must surface as runtime error"); + + match report { + CrashReport::VmPanic { message, pc } => { + assert!(message.contains("HOSTCALL 0 reached execution without loader patching")); + assert!(pc.is_some()); + } + other => panic!("expected VmPanic crash report, got {:?}", other), + } + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. }))); + } + + #[test] + fn initialize_vm_success_clears_previous_crash_report() { + let mut runtime = VirtualMachineRuntime::new(None); + runtime.last_crash_report = + Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) }); + + let mut vm = VirtualMachine::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + assert!(runtime.last_crash_report.is_none()); + } + + #[test] + fn reset_clears_cartridge_scoped_runtime_state() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + + runtime.tick_index = 77; + runtime.logical_frame_index = 12; + runtime.logical_frame_active = true; + runtime.logical_frame_remaining_cycles = 345; + runtime.last_frame_cpu_time_us = 678; + runtime.fs_state = FsState::Mounted; + runtime.open_files.insert(7, "/save.dat".into()); + runtime.next_handle = 9; + runtime.current_app_id = 42; + runtime.current_cartridge_title = "Cart".into(); + runtime.current_cartridge_app_version = "1.2.3".into(); + runtime.current_cartridge_app_mode = AppMode::System; + runtime.current_entrypoint = "main".into(); + runtime.logs_written_this_frame.insert(42, 3); + runtime.telemetry_current.frame_index = 8; + runtime.telemetry_current.cycles_used = 99; + runtime.telemetry_last.frame_index = 7; + runtime.telemetry_last.completed_logical_frames = 2; + runtime.last_crash_report = + Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(55) }); + runtime.paused = true; + runtime.debug_step_request = true; + runtime.needs_prepare_entry_call = true; + + runtime.reset(&mut vm); + + assert_eq!(runtime.tick_index, 77); + assert_eq!(runtime.fs_state, FsState::Mounted); + assert_eq!(runtime.logical_frame_index, 0); + assert!(!runtime.logical_frame_active); + assert_eq!(runtime.logical_frame_remaining_cycles, 0); + assert_eq!(runtime.last_frame_cpu_time_us, 0); + assert!(runtime.open_files.is_empty()); + assert_eq!(runtime.next_handle, 1); + assert_eq!(runtime.current_app_id, 0); + assert!(runtime.current_cartridge_title.is_empty()); + assert!(runtime.current_cartridge_app_version.is_empty()); + assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); + assert!(runtime.current_entrypoint.is_empty()); + assert!(runtime.logs_written_this_frame.is_empty()); + assert_eq!(runtime.telemetry_current.frame_index, 0); + assert_eq!(runtime.telemetry_current.cycles_used, 0); + assert_eq!(runtime.telemetry_last.frame_index, 0); + assert_eq!(runtime.telemetry_last.completed_logical_frames, 0); + assert!(runtime.last_crash_report.is_none()); + assert!(!runtime.paused); + assert!(!runtime.debug_step_request); + assert!(!runtime.needs_prepare_entry_call); + assert_eq!(vm.pc(), 0); + } + + #[test] + fn initialize_vm_failure_clears_previous_identity_and_handles() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + + let good_program = serialized_single_function_module( + assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let good_cartridge = cartridge_with_program(good_program, caps::GFX); + runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize"); + + runtime.open_files.insert(5, "/save.dat".into()); + runtime.next_handle = 6; + runtime.paused = true; + runtime.debug_step_request = true; + runtime.telemetry_current.cycles_used = 123; + + let bad_program = serialized_single_function_module( + assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let bad_cartridge = cartridge_with_program(bad_program, caps::NONE); + + let res = runtime.initialize_vm(&mut vm, &bad_cartridge); + + assert!(matches!( + res, + Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }) + )); + assert_eq!(runtime.current_app_id, 0); + assert!(runtime.current_cartridge_title.is_empty()); + assert!(runtime.current_cartridge_app_version.is_empty()); + assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); + assert!(runtime.current_entrypoint.is_empty()); + assert!(runtime.open_files.is_empty()); + assert_eq!(runtime.next_handle, 1); + assert!(!runtime.paused); + assert!(!runtime.debug_step_request); + assert_eq!(runtime.telemetry_current.cycles_used, 0); + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmInit { .. }))); + } } diff --git a/crates/console/prometeu-vm/src/heap.rs b/crates/console/prometeu-vm/src/heap.rs index 83075322..1476e0e3 100644 --- a/crates/console/prometeu-vm/src/heap.rs +++ b/crates/console/prometeu-vm/src/heap.rs @@ -47,11 +47,25 @@ pub struct CoroutineData { pub struct Heap { // Tombstone-aware store: Some(obj) = live allocation; None = freed slot. objects: Vec>, + // Reclaimed slots available for deterministic reuse (LIFO). + free_list: Vec, } impl Heap { pub fn new() -> Self { - Self { objects: Vec::new() } + Self { objects: Vec::new(), free_list: Vec::new() } + } + + fn insert_object(&mut self, obj: StoredObject) -> HeapRef { + if let Some(idx) = self.free_list.pop() { + debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none())); + self.objects[idx] = Some(obj); + HeapRef(idx as u32) + } else { + let idx = self.objects.len(); + self.objects.push(Some(obj)); + HeapRef(idx as u32) + } } /// Allocate a new object with the given kind and raw payload bytes. @@ -66,10 +80,7 @@ impl Heap { closure_env: None, coroutine: None, }; - let idx = self.objects.len(); - // No free-list reuse in this PR: append and keep indices stable. - self.objects.push(Some(obj)); - HeapRef(idx as u32) + self.insert_object(obj) } /// Allocate a new `Array` object with the given `Value` elements. @@ -84,10 +95,7 @@ impl Heap { closure_env: None, coroutine: None, }; - let idx = self.objects.len(); - // No free-list reuse in this PR: append and keep indices stable. - self.objects.push(Some(obj)); - HeapRef(idx as u32) + self.insert_object(obj) } /// Allocate a new `Closure` object with the given function id and captured environment. @@ -108,9 +116,7 @@ impl Heap { closure_env: Some(env_values.to_vec()), coroutine: None, }; - let idx = self.objects.len(); - self.objects.push(Some(obj)); - HeapRef(idx as u32) + self.insert_object(obj) } /// Allocate a new `Coroutine` object with provided initial data. @@ -131,9 +137,7 @@ impl Heap { closure_env: None, coroutine: Some(CoroutineData { pc, state, wake_tick, stack, frames }), }; - let idx = self.objects.len(); - self.objects.push(Some(obj)); - HeapRef(idx as u32) + self.insert_object(obj) } /// Returns true if this handle refers to an allocated object. @@ -352,7 +356,7 @@ impl Heap { /// tombstones (None), and clear the mark bit on the remaining live ones /// to prepare for the next GC cycle. Does not move or compact objects. pub fn sweep(&mut self) { - for slot in self.objects.iter_mut() { + for (idx, slot) in self.objects.iter_mut().enumerate() { if let Some(obj) = slot { if obj.header.is_marked() { // Live: clear mark for next cycle. @@ -360,6 +364,7 @@ impl Heap { } else { // Unreachable: reclaim by dropping and turning into tombstone. *slot = None; + self.free_list.push(idx); } } } @@ -603,6 +608,65 @@ mod tests { assert_eq!(a.0, a.0); // placeholder sanity check } + #[test] + fn sweep_reuses_freed_slot_on_next_allocation() { + let mut heap = Heap::new(); + + let dead = heap.allocate_object(ObjectKind::String, b"dead"); + let live = heap.allocate_object(ObjectKind::String, b"live"); + + heap.mark_from_roots([live]); + heap.sweep(); + + assert!(!heap.is_valid(dead)); + assert_eq!(heap.free_list, vec![dead.0 as usize]); + + let reused = heap.allocate_object(ObjectKind::Bytes, &[1, 2, 3]); + assert_eq!(reused, dead); + assert!(heap.is_valid(reused)); + assert!(heap.is_valid(live)); + } + + #[test] + fn live_handles_remain_stable_when_freelist_is_reused() { + let mut heap = Heap::new(); + + let live = heap.allocate_object(ObjectKind::String, b"live"); + let dead = heap.allocate_object(ObjectKind::String, b"dead"); + + heap.mark_from_roots([live]); + heap.sweep(); + + let replacement = heap.allocate_object(ObjectKind::Bytes, &[9]); + + assert_eq!(replacement, dead); + assert_eq!(heap.header(live).unwrap().kind, ObjectKind::String); + assert_eq!(heap.header(replacement).unwrap().kind, ObjectKind::Bytes); + assert_eq!(live.0, 0); + } + + #[test] + fn freelist_reuse_is_deterministic_lifo() { + let mut heap = Heap::new(); + + let a = heap.allocate_object(ObjectKind::String, b"a"); + let b = heap.allocate_object(ObjectKind::String, b"b"); + let c = heap.allocate_object(ObjectKind::String, b"c"); + + heap.mark_from_roots([]); + heap.sweep(); + + assert_eq!(heap.free_list, vec![a.0 as usize, b.0 as usize, c.0 as usize]); + + let r1 = heap.allocate_object(ObjectKind::Bytes, &[1]); + let r2 = heap.allocate_object(ObjectKind::Bytes, &[2]); + let r3 = heap.allocate_object(ObjectKind::Bytes, &[3]); + + assert_eq!(r1, c); + assert_eq!(r2, b); + assert_eq!(r3, a); + } + #[test] fn sweep_reclaims_unrooted_cycle() { let mut heap = Heap::new(); diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 4aaaf033..0c46dec7 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -2,20 +2,20 @@ use crate::call_frame::CallFrame; use crate::heap::{CoroutineState, Heap}; use crate::lookup_intrinsic_by_id; use crate::object::ObjectKind; -use crate::roots::{RootVisitor, visit_value_for_roots}; +use crate::roots::{visit_value_for_roots, RootVisitor}; use crate::scheduler::Scheduler; use crate::verifier::Verifier; use crate::vm_init_error::{LoaderPatchError, VmInitError}; use crate::{HostContext, NativeInterface}; -use prometeu_bytecode::HeapRef; -use prometeu_bytecode::ProgramImage; -use prometeu_bytecode::Value; use prometeu_bytecode::decode_next; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::model::BytecodeModule; +use prometeu_bytecode::HeapRef; +use prometeu_bytecode::ProgramImage; +use prometeu_bytecode::Value; use prometeu_bytecode::{ - TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, - TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, + TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_EXPLICIT, TRAP_INVALID_FUNC, + TRAP_INVALID_INTRINSIC, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, }; use prometeu_hal::syscalls::caps::NONE; use prometeu_hal::vm_fault::VmFault; @@ -110,7 +110,6 @@ pub enum LogicalFrameEndingReason { pub(crate) enum OpError { Trap(u32, String), - Panic(String), } impl From for LogicalFrameEndingReason { @@ -355,7 +354,8 @@ impl VirtualMachine { 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 + 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(); @@ -615,7 +615,8 @@ impl VirtualMachine { .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; - let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; + let val = + self.pop_trap(opcode, start_pc as u32, "JMP_IF_FALSE requires one operand")?; match val { Value::Boolean(false) => { let func_start = self @@ -641,7 +642,8 @@ impl VirtualMachine { .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; - let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; + let val = + self.pop_trap(opcode, start_pc as u32, "JMP_IF_TRUE requires one operand")?; match val { Value::Boolean(true) => { let func_start = self @@ -663,9 +665,14 @@ impl VirtualMachine { } } OpCode::Trap => { - // Manual breakpoint instruction: consume cycles and signal a breakpoint + // TRAP is guest-visible ISA, distinct from debugger breakpoints. self.cycles += OpCode::Trap.cycles(); - return Err(LogicalFrameEndingReason::Breakpoint); + return Err(self.trap( + TRAP_EXPLICIT, + opcode as u16, + "Program requested trap".into(), + start_pc as u32, + )); } OpCode::Spawn => { // Operands: (fn_id, arg_count) @@ -701,18 +708,27 @@ impl VirtualMachine { } 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() - ))); + return Err(self.trap( + TRAP_STACK_UNDERFLOW, + opcode as u16, + format!( + "Stack underflow during SPAWN to func {}: expected at least {} arguments, got {}", + fn_id, + arg_count, + self.operand_stack.len() + ), + start_pc as u32, + )); } // 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.push(self.pop_trap( + opcode, + start_pc as u32, + format!("SPAWN to func {} argument stack underflow", fn_id), + )?); } args.reverse(); @@ -790,7 +806,11 @@ impl VirtualMachine { // 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))?; + let v = self.pop_trap( + opcode, + start_pc as u32, + "MAKE_CLOSURE capture stack underflow", + )?; temp.push(v); } // Preserve order so that env[0] corresponds to captured_1 (the bottom-most @@ -809,7 +829,11 @@ impl VirtualMachine { as usize; // Pop the closure reference from the stack (top of stack). - let clos_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let clos_val = self.pop_trap( + opcode, + start_pc as u32, + "CALL_CLOSURE requires a closure handle on the stack", + )?; let href = match clos_val { Value::HeapRef(h) => h, other => { @@ -846,7 +870,11 @@ impl VirtualMachine { // 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.push(self.pop_trap( + opcode, + start_pc as u32, + "CALL_CLOSURE argument stack underflow", + )?); } user_args.reverse(); // Now in logical order: arg1..argN @@ -915,7 +943,12 @@ impl VirtualMachine { .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.trap( + TRAP_OOB, + opcode as u16, + format!("Invalid constant index {}", idx), + start_pc as u32, + ) })?; self.push(val); } @@ -944,23 +977,24 @@ impl VirtualMachine { self.push(Value::Boolean(val != 0)); } OpCode::Pop => { - self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + self.pop_trap(opcode, start_pc as u32, "POP requires one operand")?; } 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))?; + self.pop_trap(opcode, start_pc as u32, "POPN operand stack underflow")?; } } OpCode::Dup => { - let val = self.peek().map_err(|e| LogicalFrameEndingReason::Panic(e))?.clone(); + let val = + self.peek_trap(opcode, start_pc as u32, "DUP requires one operand")?.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))?; + let a = self.pop_trap(opcode, start_pc as u32, "SWAP requires two operands")?; + let b = self.pop_trap(opcode, start_pc as u32, "SWAP requires two operands")?; self.push(a); self.push(b); } @@ -979,7 +1013,7 @@ impl VirtualMachine { (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)), - _ => Err(OpError::Panic("Invalid types for ADD".into())), + _ => Err(OpError::Trap(TRAP_TYPE, "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))), @@ -991,7 +1025,7 @@ impl VirtualMachine { (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)), - _ => Err(OpError::Panic("Invalid types for SUB".into())), + _ => Err(OpError::Trap(TRAP_TYPE, "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))), @@ -1003,7 +1037,7 @@ impl VirtualMachine { (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)), - _ => Err(OpError::Panic("Invalid types for MUL".into())), + _ => Err(OpError::Trap(TRAP_TYPE, "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)) => { @@ -1072,7 +1106,7 @@ impl VirtualMachine { } Ok(Value::Float(a / b as f64)) } - _ => Err(OpError::Panic("Invalid types for DIV".into())), + _ => Err(OpError::Trap(TRAP_TYPE, "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)) => { @@ -1087,7 +1121,7 @@ impl VirtualMachine { } Ok(Value::Int64(a % b)) } - _ => Err(OpError::Panic("Invalid types for MOD".into())), + _ => Err(OpError::Trap(TRAP_TYPE, "Invalid types for MOD".into())), })?, OpCode::Eq => { self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))? @@ -1098,37 +1132,42 @@ impl VirtualMachine { 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())) + .ok_or_else(|| OpError::Trap(TRAP_TYPE, "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())) + .ok_or_else(|| OpError::Trap(TRAP_TYPE, "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())) + .ok_or_else(|| OpError::Trap(TRAP_TYPE, "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())) + .ok_or_else(|| OpError::Trap(TRAP_TYPE, "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())), + _ => Err(OpError::Trap(TRAP_TYPE, "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())), + _ => Err(OpError::Trap(TRAP_TYPE, "Invalid types for OR".into())), })?, OpCode::Not => { - let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; + let val = self.pop_trap(opcode, start_pc as u32, "NOT requires one operand")?; if let Value::Boolean(b) = val { self.push(Value::Boolean(!b)); } else { - return Err(LogicalFrameEndingReason::Panic("Invalid type for NOT".into())); + return Err(self.trap( + TRAP_TYPE, + opcode as u16, + "Invalid type for NOT".into(), + start_pc as u32, + )); } } OpCode::BitAnd => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { @@ -1136,21 +1175,21 @@ impl VirtualMachine { (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())), + _ => Err(OpError::Trap(TRAP_TYPE, "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())), + _ => Err(OpError::Trap(TRAP_TYPE, "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())), + _ => Err(OpError::Trap(TRAP_TYPE, "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))), @@ -1159,7 +1198,7 @@ impl VirtualMachine { 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())), + _ => Err(OpError::Trap(TRAP_TYPE, "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))), @@ -1168,16 +1207,21 @@ impl VirtualMachine { 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())), + _ => Err(OpError::Trap(TRAP_TYPE, "Invalid types for Shr".into())), })?, OpCode::Neg => { - let val = self.pop().map_err(LogicalFrameEndingReason::Panic)?; + let val = self.pop_trap(opcode, start_pc as u32, "NEG requires one operand")?; 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())); + return Err(self.trap( + TRAP_TYPE, + opcode as u16, + "Invalid type for Neg".into(), + start_pc as u32, + )); } } } @@ -1197,7 +1241,8 @@ impl VirtualMachine { .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; - let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let val = + self.pop_trap(opcode, start_pc as u32, "SET_GLOBAL requires one operand")?; if idx >= self.globals.len() { self.globals.resize(idx + 1, Value::Int32(0)); } @@ -1234,7 +1279,8 @@ impl VirtualMachine { let slot = instr .imm_u32() .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; - let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let val = + self.pop_trap(opcode, start_pc as u32, "SET_LOCAL requires one operand")?; let frame = self.call_stack.last().ok_or_else(|| { LogicalFrameEndingReason::Panic("No active call frame".into()) })?; @@ -1268,12 +1314,17 @@ impl VirtualMachine { })?; 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() - ))); + return Err(self.trap( + TRAP_STACK_UNDERFLOW, + opcode as u16, + format!( + "Stack underflow during CALL to func {}: expected at least {} arguments, got {}", + func_id, + callee.param_slots, + self.operand_stack.len() + ), + start_pc as u32, + )); } let stack_base = self.operand_stack.len() - callee.param_slots as usize; @@ -1292,7 +1343,12 @@ impl VirtualMachine { } OpCode::Ret => { let frame = self.call_stack.pop().ok_or_else(|| { - LogicalFrameEndingReason::Panic("Call stack underflow".into()) + self.trap( + TRAP_STACK_UNDERFLOW, + opcode as u16, + "RET with empty call stack".into(), + start_pc as u32, + ) })?; let func = &self.program.functions[frame.func_idx]; let return_slots = func.return_slots as usize; @@ -1649,6 +1705,24 @@ impl VirtualMachine { self.operand_stack.last().ok_or("Stack underflow".into()) } + fn pop_trap>( + &mut self, + opcode: OpCode, + pc: u32, + message: S, + ) -> Result { + self.pop().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc)) + } + + fn peek_trap>( + &self, + opcode: OpCode, + pc: u32, + message: S, + ) -> Result<&Value, LogicalFrameEndingReason> { + self.peek().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc)) + } + fn binary_op( &mut self, opcode: OpCode, @@ -1658,15 +1732,14 @@ impl VirtualMachine { 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))?; + let b = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?; + let a = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?; 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)), } } @@ -1726,7 +1799,8 @@ mod tests { use crate::HostReturn; use prometeu_bytecode::model::{BytecodeModule, SyscallDecl}; use prometeu_bytecode::{ - FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW, assemble, disassemble, + assemble, disassemble, FunctionMeta, TRAP_EXPLICIT, TRAP_INVALID_LOCAL, TRAP_OOB, + TRAP_STACK_UNDERFLOW, TRAP_TYPE, }; use prometeu_hal::expect_int; @@ -2128,6 +2202,29 @@ mod tests { assert_eq!(vm.peek().unwrap(), &Value::String("hello".into())); } + #[test] + fn test_push_const_invalid_index_traps_oob() { + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushConst 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, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::PushConst as u16); + assert!(trap.message.contains("Invalid constant index")); + } + other => panic!("Expected Trap(OOB), got {:?}", other), + } + } + #[test] fn test_call_ret_scope_separation() { let mut rom = Vec::new(); @@ -2441,11 +2538,40 @@ mod tests { 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); + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, TRAP_EXPLICIT); + assert_eq!(trap.opcode, OpCode::Trap as u16); + assert_eq!(trap.pc, 6); + assert!(trap.message.contains("Program requested trap")); + } + other => panic!("Expected Trap(TRAP_EXPLICIT), got {:?}", other), + } assert_eq!(vm.pc, 8); // PushI32 (6 bytes) + Trap (2 bytes) assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); } + #[test] + fn test_debugger_breakpoint_remains_distinct_from_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, vec![]); + vm.insert_breakpoint(6); + + let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); + + assert_eq!(report.reason, LogicalFrameEndingReason::Breakpoint); + assert_eq!(vm.pc, 6); + assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); + } + #[test] fn test_pop_n_opcode() { let mut native = MockNative; @@ -2634,6 +2760,122 @@ mod tests { } } + #[test] + fn test_add_invalid_types_traps_type() { + let rom = assemble("PUSH_I32 1\nPUSH_BOOL 1\nADD\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::Add as u16); + assert!(trap.message.contains("Invalid types for ADD")); + } + other => panic!("Expected Trap(TYPE), got {:?}", other), + } + } + + #[test] + fn test_add_stack_underflow_traps() { + let rom = assemble("PUSH_I32 1\nADD\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::Add as u16); + assert!(trap.message.contains("requires two operands")); + } + other => panic!("Expected Trap(STACK_UNDERFLOW), got {:?}", other), + } + } + + #[test] + fn test_and_invalid_types_traps_type() { + let rom = assemble("PUSH_I32 1\nPUSH_BOOL 1\nAND\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::And as u16); + assert!(trap.message.contains("Invalid types for AND")); + } + other => panic!("Expected Trap(TYPE), got {:?}", other), + } + } + + #[test] + fn test_not_invalid_type_traps_type() { + let rom = assemble("PUSH_I32 1\nNOT\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::Not as u16); + assert!(trap.message.contains("Invalid type for NOT")); + } + other => panic!("Expected Trap(TYPE), got {:?}", other), + } + } + + #[test] + fn test_neg_invalid_type_traps_type() { + let rom = assemble("PUSH_BOOL 1\nNEG\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::Neg as u16); + assert!(trap.message.contains("Invalid type for Neg")); + } + other => panic!("Expected Trap(TYPE), got {:?}", other), + } + } + + #[test] + fn test_pop_underflow_traps() { + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::Pop as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + 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::Pop as u16); + assert!(trap.message.contains("POP requires one operand")); + } + other => panic!("Expected Trap(STACK_UNDERFLOW), got {:?}", other), + } + } + #[test] fn test_invalid_syscall_trap() { let rom = vec![ diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index dec3cd04..626769c5 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -2,6 +2,7 @@ use prometeu_drivers::hardware::Hardware; use prometeu_firmware::{BootTarget, Firmware}; use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::debugger_protocol::*; +use prometeu_system::CrashReport; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; @@ -22,6 +23,8 @@ pub struct HostDebugger { last_log_seq: u64, /// Frame tracker to send telemetry snapshots periodically. last_telemetry_frame: u64, + /// Last fault summary sent to the debugger client. + last_fault_summary: Option, } impl HostDebugger { @@ -33,6 +36,7 @@ impl HostDebugger { stream: None, last_log_seq: 0, last_telemetry_frame: 0, + last_fault_summary: None, } } @@ -96,6 +100,7 @@ impl HostDebugger { stream.set_nonblocking(true).expect("Cannot set non-blocking on stream"); self.stream = Some(stream); + self.last_fault_summary = None; // Immediately send the Handshake message to identify the Runtime and App. let handshake = DebugResponse::Handshake { @@ -221,6 +226,12 @@ impl HostDebugger { /// Scans the system for new information to push to the debugger client. fn stream_events(&mut self, firmware: &mut Firmware) { + if let Some(report) = firmware.os.last_crash_report.as_ref() { + self.stream_fault(report); + } else { + self.last_fault_summary = None; + } + // 1. Process and send new log entries. let new_events = firmware.os.log_service.get_after(self.last_log_seq); for event in new_events { @@ -295,4 +306,25 @@ impl HostDebugger { self.last_telemetry_frame = current_frame; } } + + fn stream_fault(&mut self, report: &CrashReport) { + let summary = report.summary(); + if self.last_fault_summary.as_deref() == Some(summary.as_str()) { + return; + } + + let (trap_code, opcode) = match report { + CrashReport::VmTrap { trap } => (Some(trap.code), Some(trap.opcode)), + CrashReport::VmPanic { .. } | CrashReport::VmInit { .. } => (None, None), + }; + + self.send_event(DebugEvent::Fault { + kind: report.kind().to_string(), + summary: summary.clone(), + pc: report.pc(), + trap_code, + opcode, + }); + self.last_fault_summary = Some(summary); + } } diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index a4ccc5e8..00c4089c 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -207,6 +207,14 @@ impl HostRunner { self.hardware.gfx.draw_text(10, 90, &msg, color_warn); } } + + if let Some(report) = self.firmware.os.last_crash_report.as_ref() { + let mut msg = report.summary(); + if msg.len() > 30 { + msg.truncate(30); + } + self.hardware.gfx.draw_text(10, 98, &msg, color_warn); + } } } diff --git a/docs/pull-requests/PR-013-[DEBUGGER]-consume-structured-fault-events.md b/docs/pull-requests/PR-013-[DEBUGGER]-consume-structured-fault-events.md new file mode 100644 index 00000000..897d00f7 --- /dev/null +++ b/docs/pull-requests/PR-013-[DEBUGGER]-consume-structured-fault-events.md @@ -0,0 +1,67 @@ +# PR-013 [DEBUGGER]: Consume Structured Fault Events + +## Briefing + +O runtime e o host agora produzem `CrashReport` e emitem evento `fault` estruturado no protocolo do debugger. Mas o lado consumidor do DevTools ainda nao foi ajustado para tratar esse evento como dado de primeira classe. Sem isso, o debugger continua dependente de logs gerais ou de texto livre para exibir falhas da VM. + +Esta PR fecha a ponta do debugger: faults estruturados passam a ser recebidos, armazenados e renderizados como diagnostico explicito, separado de logs e telemetria numerica. + +## Problema + +- o protocolo ja expõe `DebugEvent::Fault`, mas o cliente do debugger ainda pode ignorar esse evento; +- traps, panics e falhas de init continuam sem superficie dedicada no tooling; +- o operador do debugger nao consegue distinguir com clareza: + - log normal; + - violacao de certificacao; + - fault terminal da VM. + +## Alvo + +- cliente do debugger / DevTools que consome `DebugEvent` +- componentes de UI/estado que exibem logs, telemetria e status de execucao +- testes do lado cliente do debugger, se existirem no repo + +## Escopo + +- Consumir `DebugEvent::Fault` como evento estruturado, nao como log generico. +- Exibir pelo menos: + - tipo (`vm_trap`, `vm_panic`, `vm_init`); + - resumo; + - `pc`, quando existir; + - `trap_code` e `opcode`, quando existirem. +- Manter faults historicos recentes no estado do debugger de forma simples e deterministica. +- Garantir que um fault terminal fique visualmente distinguivel de log comum. + +## Fora de Escopo + +- Redesenhar toda a UI do debugger. +- Alterar novamente o protocolo do host, salvo ajuste minimo estritamente necessario. +- Implementar crash screen dentro da VM/firmware. +- Introduzir persistencia em disco de faults do debugger. + +## Abordagem + +1. Auditar o cliente do debugger e localizar onde `DebugEvent` e desserializado e roteado. +2. Adicionar tratamento explicito para `fault`, com estado proprio no cliente. +3. Renderizar o fault em area dedicada ou secao claramente separada de logs. +4. Preservar os campos estruturados; nenhuma logica deve depender de parsear `summary`. +5. Se houver historico, limitar o buffer de faults para manter simplicidade e previsibilidade. + +## Criterios de Aceite + +- O debugger reconhece `DebugEvent::Fault`. +- Um `VmTrap` aparece com `trap_code`, `opcode`, `pc` e resumo. +- Um `VmPanic` aparece distinto de `VmTrap`, sem depender de convencao textual. +- Um `VmInit` aparece distinto de fault de runtime. +- O fluxo de logs continua funcionando sem regressao. + +## Tests + +- Teste de desserializacao/roteamento do cliente cobrindo evento `fault`. +- Teste de estado/UI garantindo que `VmTrap` e `VmPanic` aparecem em superficie dedicada. +- Se houver suite de frontend/cliente, rodar a suite relevante do debugger. +- Se o cliente estiver no mesmo crate do host, incluir ao menos teste unitario do handler de eventos. + +## Risco + +Baixo a medio. O maior risco e introduzir uma segunda representacao local de fault que volte a colapsar semantica em texto. A implementacao deve reaproveitar o protocolo estruturado ja existente e manter a taxonomia pequena e estavel.