implements PR-007 to PR011

This commit is contained in:
bQUARKz 2026-03-03 09:51:01 +00:00
parent c0ed9ba8e0
commit a75df486ce
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
19 changed files with 978 additions and 117 deletions

1
Cargo.lock generated
View File

@ -1630,6 +1630,7 @@ dependencies = [
name = "prometeu-firmware" name = "prometeu-firmware"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"prometeu-bytecode",
"prometeu-drivers", "prometeu-drivers",
"prometeu-hal", "prometeu-hal",
"prometeu-system", "prometeu-system",

View File

@ -3,6 +3,8 @@
/// Attempted to execute an unknown or invalid opcode. /// Attempted to execute an unknown or invalid opcode.
pub const TRAP_ILLEGAL_INSTRUCTION: u32 = 0x0000_0001; 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). /// Out-of-bounds access (e.g., stack/heap/local index out of range).
pub const TRAP_OOB: u32 = 0x0000_0003; pub const TRAP_OOB: u32 = 0x0000_0003;
/// Type mismatch for the attempted operation (e.g., wrong operand type or syscall argument type). /// Type mismatch for the attempted operation (e.g., wrong operand type or syscall argument type).

View File

@ -11,8 +11,8 @@ mod program_image;
mod value; mod value;
pub use abi::{ pub use abi::{
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC, TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_EXPLICIT, TRAP_ILLEGAL_INSTRUCTION,
TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB,
TRAP_STACK_UNDERFLOW, TRAP_TYPE, TRAP_STACK_UNDERFLOW, TRAP_TYPE,
}; };
pub use assembler::{assemble, AsmError}; pub use assembler::{assemble, AsmError};

View File

@ -11,3 +11,4 @@ prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies] [dev-dependencies]
prometeu-drivers = { path = "../prometeu-drivers" } prometeu-drivers = { path = "../prometeu-drivers" }
prometeu-bytecode = { path = "../prometeu-bytecode" }

View File

@ -168,8 +168,12 @@ impl Firmware {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware; use prometeu_drivers::hardware::Hardware;
use prometeu_hal::cartridge::AppMode; use prometeu_hal::cartridge::AppMode;
use prometeu_hal::syscalls::caps;
use prometeu_system::CrashReport;
fn invalid_game_cartridge() -> Cartridge { fn invalid_game_cartridge() -> 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] #[test]
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
let mut firmware = Firmware::new(None); let mut firmware = Firmware::new(None);
@ -196,9 +237,36 @@ mod tests {
firmware.tick(&signals, &mut hardware); firmware.tick(&signals, &mut hardware);
match &firmware.state { match &firmware.state {
FirmwareState::AppCrashes(step) => { FirmwareState::AppCrashes(step) => match &step.report {
assert!(step.error.contains("InvalidFormat")); 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), other => panic!("expected AppCrashes state, got {:?}", other),
} }
} }

View File

@ -2,15 +2,20 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext; use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_system::CrashReport;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppCrashesStep { pub struct AppCrashesStep {
pub error: String, pub report: CrashReport,
} }
impl AppCrashesStep { impl AppCrashesStep {
pub fn log_message(&self) -> String {
format!("App Crashed: {}", self.report)
}
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { 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<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
@ -33,3 +38,30 @@ impl AppCrashesStep {
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {} 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"));
}
}

View File

@ -17,7 +17,7 @@ impl GameRunningStep {
ctx.hw.gfx_mut().present(); 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) {} pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}

View File

@ -30,8 +30,8 @@ impl HubHomeStep {
ctx.hw.gfx_mut().present(); ctx.hw.gfx_mut().present();
if let Some(err) = error { if let Some(report) = error {
return Some(FirmwareState::AppCrashes(AppCrashesStep { error: err })); return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
} }
None None

View File

@ -6,11 +6,12 @@ use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect; use prometeu_hal::window::Rect;
use prometeu_system::CrashReport;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LoadCartridgeStep { pub struct LoadCartridgeStep {
pub cartridge: Cartridge, pub cartridge: Cartridge,
init_error: Option<String>, init_error: Option<CrashReport>,
} }
impl LoadCartridgeStep { impl LoadCartridgeStep {
@ -33,13 +34,12 @@ impl LoadCartridgeStep {
self.cartridge.assets.clone(), self.cartridge.assets.clone(),
); );
self.init_error = self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err();
ctx.os.initialize_vm(ctx.vm, &self.cartridge).err().map(|err| format!("{:?}", err));
} }
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
if let Some(error) = self.init_error.take() { if let Some(report) = self.init_error.take() {
return Some(FirmwareState::AppCrashes(AppCrashesStep { error })); return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
} }
if self.cartridge.app_mode == AppMode::System { if self.cartridge.app_mode == AppMode::System {

View File

@ -71,6 +71,14 @@ pub enum DebugEvent {
audio_inflight_bytes: usize, audio_inflight_bytes: usize,
audio_slots_occupied: u32, audio_slots_occupied: u32,
}, },
#[serde(rename = "fault")]
Fault {
kind: String,
summary: String,
pc: Option<u32>,
trap_code: Option<u32>,
opcode: Option<u16>,
},
#[serde(rename = "cert")] #[serde(rename = "cert")]
Cert { rule: String, used: u64, limit: u64, frame_index: u64 }, 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("\"frame_index\":5"));
assert!(json.contains("\"app_id\":1")); 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"));
}
} }

View File

@ -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<u32> },
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<u32> {
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())
}
}

View File

@ -1,7 +1,9 @@
mod crash_report;
mod programs; mod programs;
mod services; mod services;
mod virtual_machine_runtime; mod virtual_machine_runtime;
pub use crash_report::CrashReport;
pub use programs::PrometeuHub; pub use programs::PrometeuHub;
pub use services::fs; pub use services::fs;
pub use virtual_machine_runtime::VirtualMachineRuntime; pub use virtual_machine_runtime::VirtualMachineRuntime;

View File

@ -1,9 +1,9 @@
use crate::VirtualMachineRuntime;
use crate::programs::prometeu_hub::window_manager::WindowManager; use crate::programs::prometeu_hub::window_manager::WindowManager;
use prometeu_hal::HardwareBridge; use crate::VirtualMachineRuntime;
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect; use prometeu_hal::window::Rect;
use prometeu_hal::HardwareBridge;
/// PrometeuHub: Launcher and system UI environment. /// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub { pub struct PrometeuHub {

View File

@ -1,5 +1,6 @@
use crate::fs::{FsBackend, FsState, VirtualFS}; 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::asset::{BankType, LoadStatus, SlotRef};
use prometeu_hal::button::Button; use prometeu_hal::button::Button;
use prometeu_hal::cartridge::{AppMode, Cartridge}; 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::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use prometeu_hal::tile::Tile; use prometeu_hal::tile::Tile;
use prometeu_hal::vm_fault::VmFault; 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::{HardwareBridge, InputSignals};
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine, VmInitError};
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Instant; use std::time::Instant;
@ -55,6 +56,8 @@ pub struct VirtualMachineRuntime {
pub telemetry_current: TelemetryFrame, pub telemetry_current: TelemetryFrame,
/// The results of the last successfully completed logical frame. /// The results of the last successfully completed logical frame.
pub telemetry_last: TelemetryFrame, pub telemetry_last: TelemetryFrame,
/// Last terminal crash report surfaced by the runtime, if any.
pub last_crash_report: Option<CrashReport>,
/// Logic for validating that the app obeys the console's Certification (CAP). /// Logic for validating that the app obeys the console's Certification (CAP).
pub certifier: Certifier, pub certifier: Certifier,
/// Pause state: When true, `tick()` will not advance the VM. /// Pause state: When true, `tick()` will not advance the VM.
@ -104,6 +107,7 @@ impl VirtualMachineRuntime {
logs_written_this_frame: HashMap::new(), logs_written_this_frame: HashMap::new(),
telemetry_current: TelemetryFrame::default(), telemetry_current: TelemetryFrame::default(),
telemetry_last: TelemetryFrame::default(), telemetry_last: TelemetryFrame::default(),
last_crash_report: None,
certifier: Certifier::new(cap_config.unwrap_or_default()), certifier: Certifier::new(cap_config.unwrap_or_default()),
paused: false, paused: false,
debug_step_request: false, debug_step_request: false,
@ -161,13 +165,34 @@ impl VirtualMachineRuntime {
} }
} }
pub fn reset(&mut self, vm: &mut VirtualMachine) { fn clear_cartridge_state(&mut self) {
*vm = VirtualMachine::default();
self.tick_index = 0;
self.logical_frame_index = 0; self.logical_frame_index = 0;
self.logical_frame_active = false; self.logical_frame_active = false;
self.logical_frame_remaining_cycles = 0; self.logical_frame_remaining_cycles = 0;
self.last_frame_cpu_time_us = 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. /// Loads a cartridge into the PVM and resets the execution state.
@ -175,7 +200,8 @@ impl VirtualMachineRuntime {
&mut self, &mut self,
vm: &mut VirtualMachine, vm: &mut VirtualMachine,
cartridge: &Cartridge, cartridge: &Cartridge,
) -> Result<(), VmInitError> { ) -> Result<(), CrashReport> {
self.clear_cartridge_state();
vm.set_capabilities(cartridge.capabilities); vm.set_capabilities(cartridge.capabilities);
match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) { match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) {
@ -189,15 +215,17 @@ impl VirtualMachineRuntime {
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
let report = CrashReport::VmInit { error: e };
self.last_crash_report = Some(report.clone());
self.log( self.log(
LogLevel::Error, LogLevel::Error,
LogSource::Vm, LogSource::Vm,
0, report.log_tag(),
format!("Failed to initialize VM: {:?}", e), format!("Failed to initialize VM: {}", report),
); );
// Fail fast: no program is installed, no app id is switched. // Fail fast: no program is installed, no app id is switched.
// We don't update current_app_id or other fields. // We don't update current_app_id or other fields.
Err(e) Err(report)
} }
} }
} }
@ -207,14 +235,29 @@ impl VirtualMachineRuntime {
&mut self, &mut self,
vm: &mut VirtualMachine, vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge, hw: &mut dyn HardwareBridge,
) -> Option<String> { ) -> Option<CrashReport> {
let mut ctx = HostContext::new(Some(hw)); let mut ctx = HostContext::new(Some(hw));
match vm.step(self, &mut ctx) { match vm.step(self, &mut ctx) {
Ok(_) => None, Ok(_) => None,
Err(e) => { Err(e) => {
let err_msg = format!("PVM Fault during Step: {:?}", e); let report = match e {
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); LogicalFrameEndingReason::Trap(trap) => CrashReport::VmTrap { trap },
Some(err_msg) 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, vm: &mut VirtualMachine,
signals: &InputSignals, signals: &InputSignals,
hw: &mut dyn HardwareBridge, hw: &mut dyn HardwareBridge,
) -> Option<String> { ) -> Option<CrashReport> {
let start = Instant::now(); let start = Instant::now();
self.tick_index += 1; self.tick_index += 1;
@ -303,9 +346,28 @@ impl VirtualMachineRuntime {
// Handle Panics // Handle Panics
if let LogicalFrameEndingReason::Panic(err) = run.reason { if let LogicalFrameEndingReason::Panic(err) = run.reason {
let err_msg = format!("PVM Fault: \"{}\"", err); let report =
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) };
return Some(err_msg); 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) // 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned)
@ -351,9 +413,10 @@ impl VirtualMachineRuntime {
} }
Err(e) => { Err(e) => {
// Fatal VM fault (division by zero, invalid memory access, etc). // Fatal VM fault (division by zero, invalid memory access, etc).
let err_msg = format!("PVM Fault: {:?}", e); let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) };
self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary());
return Some(err_msg); self.last_crash_report = Some(report.clone());
return Some(report);
} }
} }
} }
@ -1129,8 +1192,12 @@ mod tests {
use super::*; use super::*;
use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; 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::cartridge::Cartridge;
use prometeu_hal::syscalls::caps; use prometeu_hal::syscalls::caps;
use prometeu_hal::InputSignals;
use prometeu_vm::VmInitError;
fn cartridge_with_program(program: Vec<u8>, capabilities: u64) -> Cartridge { fn cartridge_with_program(program: Vec<u8>, capabilities: u64) -> Cartridge {
Cartridge { Cartridge {
@ -1183,7 +1250,10 @@ mod tests {
let res = runtime.initialize_vm(&mut vm, &cartridge); 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!(runtime.current_app_id, 0);
assert_eq!(vm.pc(), 0); assert_eq!(vm.pc(), 0);
assert_eq!(vm.operand_stack_top(1), Vec::<Value>::new()); assert_eq!(vm.operand_stack_top(1), Vec::<Value>::new());
@ -1212,4 +1282,196 @@ mod tests {
assert_eq!(runtime.current_app_id, 42); assert_eq!(runtime.current_app_id, 42);
assert!(!vm.is_halted()); 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 { .. })));
}
} }

View File

@ -47,11 +47,25 @@ pub struct CoroutineData {
pub struct Heap { pub struct Heap {
// Tombstone-aware store: Some(obj) = live allocation; None = freed slot. // Tombstone-aware store: Some(obj) = live allocation; None = freed slot.
objects: Vec<Option<StoredObject>>, objects: Vec<Option<StoredObject>>,
// Reclaimed slots available for deterministic reuse (LIFO).
free_list: Vec<usize>,
} }
impl Heap { impl Heap {
pub fn new() -> Self { 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. /// Allocate a new object with the given kind and raw payload bytes.
@ -66,10 +80,7 @@ impl Heap {
closure_env: None, closure_env: None,
coroutine: None, coroutine: None,
}; };
let idx = self.objects.len(); self.insert_object(obj)
// No free-list reuse in this PR: append and keep indices stable.
self.objects.push(Some(obj));
HeapRef(idx as u32)
} }
/// Allocate a new `Array` object with the given `Value` elements. /// Allocate a new `Array` object with the given `Value` elements.
@ -84,10 +95,7 @@ impl Heap {
closure_env: None, closure_env: None,
coroutine: None, coroutine: None,
}; };
let idx = self.objects.len(); self.insert_object(obj)
// No free-list reuse in this PR: append and keep indices stable.
self.objects.push(Some(obj));
HeapRef(idx as u32)
} }
/// Allocate a new `Closure` object with the given function id and captured environment. /// 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()), closure_env: Some(env_values.to_vec()),
coroutine: None, coroutine: None,
}; };
let idx = self.objects.len(); self.insert_object(obj)
self.objects.push(Some(obj));
HeapRef(idx as u32)
} }
/// Allocate a new `Coroutine` object with provided initial data. /// Allocate a new `Coroutine` object with provided initial data.
@ -131,9 +137,7 @@ impl Heap {
closure_env: None, closure_env: None,
coroutine: Some(CoroutineData { pc, state, wake_tick, stack, frames }), coroutine: Some(CoroutineData { pc, state, wake_tick, stack, frames }),
}; };
let idx = self.objects.len(); self.insert_object(obj)
self.objects.push(Some(obj));
HeapRef(idx as u32)
} }
/// Returns true if this handle refers to an allocated object. /// 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 /// 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. /// to prepare for the next GC cycle. Does not move or compact objects.
pub fn sweep(&mut self) { 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 let Some(obj) = slot {
if obj.header.is_marked() { if obj.header.is_marked() {
// Live: clear mark for next cycle. // Live: clear mark for next cycle.
@ -360,6 +364,7 @@ impl Heap {
} else { } else {
// Unreachable: reclaim by dropping and turning into tombstone. // Unreachable: reclaim by dropping and turning into tombstone.
*slot = None; *slot = None;
self.free_list.push(idx);
} }
} }
} }
@ -603,6 +608,65 @@ mod tests {
assert_eq!(a.0, a.0); // placeholder sanity check 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] #[test]
fn sweep_reclaims_unrooted_cycle() { fn sweep_reclaims_unrooted_cycle() {
let mut heap = Heap::new(); let mut heap = Heap::new();

View File

@ -2,20 +2,20 @@ use crate::call_frame::CallFrame;
use crate::heap::{CoroutineState, Heap}; use crate::heap::{CoroutineState, Heap};
use crate::lookup_intrinsic_by_id; use crate::lookup_intrinsic_by_id;
use crate::object::ObjectKind; 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::scheduler::Scheduler;
use crate::verifier::Verifier; use crate::verifier::Verifier;
use crate::vm_init_error::{LoaderPatchError, VmInitError}; use crate::vm_init_error::{LoaderPatchError, VmInitError};
use crate::{HostContext, NativeInterface}; use crate::{HostContext, NativeInterface};
use prometeu_bytecode::HeapRef;
use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value;
use prometeu_bytecode::decode_next; use prometeu_bytecode::decode_next;
use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
use prometeu_bytecode::model::BytecodeModule; use prometeu_bytecode::model::BytecodeModule;
use prometeu_bytecode::HeapRef;
use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value;
use prometeu_bytecode::{ use prometeu_bytecode::{
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_EXPLICIT, TRAP_INVALID_FUNC,
TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, TRAP_INVALID_INTRINSIC, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE,
}; };
use prometeu_hal::syscalls::caps::NONE; use prometeu_hal::syscalls::caps::NONE;
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
@ -110,7 +110,6 @@ pub enum LogicalFrameEndingReason {
pub(crate) enum OpError { pub(crate) enum OpError {
Trap(u32, String), Trap(u32, String),
Panic(String),
} }
impl From<TrapInfo> for LogicalFrameEndingReason { impl From<TrapInfo> for LogicalFrameEndingReason {
@ -355,7 +354,8 @@ impl VirtualMachine {
idx idx
} else { } else {
// Try to resolve as a symbol name // 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(); let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default();
@ -615,7 +615,8 @@ impl VirtualMachine {
.imm_u32() .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize; 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 { match val {
Value::Boolean(false) => { Value::Boolean(false) => {
let func_start = self let func_start = self
@ -641,7 +642,8 @@ impl VirtualMachine {
.imm_u32() .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize; 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 { match val {
Value::Boolean(true) => { Value::Boolean(true) => {
let func_start = self let func_start = self
@ -663,9 +665,14 @@ impl VirtualMachine {
} }
} }
OpCode::Trap => { 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(); 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 => { OpCode::Spawn => {
// Operands: (fn_id, arg_count) // Operands: (fn_id, arg_count)
@ -701,18 +708,27 @@ impl VirtualMachine {
} }
if self.operand_stack.len() < arg_count { if self.operand_stack.len() < arg_count {
return Err(LogicalFrameEndingReason::Panic(format!( return Err(self.trap(
TRAP_STACK_UNDERFLOW,
opcode as u16,
format!(
"Stack underflow during SPAWN to func {}: expected at least {} arguments, got {}", "Stack underflow during SPAWN to func {}: expected at least {} arguments, got {}",
fn_id, fn_id,
arg_count, arg_count,
self.operand_stack.len() self.operand_stack.len()
))); ),
start_pc as u32,
));
} }
// Pop args top-first, then reverse to logical order arg1..argN // Pop args top-first, then reverse to logical order arg1..argN
let mut args: Vec<Value> = Vec::with_capacity(arg_count); let mut args: Vec<Value> = Vec::with_capacity(arg_count);
for _ in 0..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(); args.reverse();
@ -790,7 +806,11 @@ impl VirtualMachine {
// Pop cap_count values from the operand stack, top-first. // Pop cap_count values from the operand stack, top-first.
let mut temp: Vec<Value> = Vec::with_capacity(cap_count as usize); let mut temp: Vec<Value> = Vec::with_capacity(cap_count as usize);
for _ in 0..cap_count { 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); temp.push(v);
} }
// Preserve order so that env[0] corresponds to captured_1 (the bottom-most // Preserve order so that env[0] corresponds to captured_1 (the bottom-most
@ -809,7 +829,11 @@ impl VirtualMachine {
as usize; as usize;
// Pop the closure reference from the stack (top of stack). // 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 { let href = match clos_val {
Value::HeapRef(h) => h, Value::HeapRef(h) => h,
other => { other => {
@ -846,7 +870,11 @@ impl VirtualMachine {
// Pop user arguments from the operand stack (top-first), then fix order. // Pop user arguments from the operand stack (top-first), then fix order.
let mut user_args: Vec<Value> = Vec::with_capacity(user_arg_count); let mut user_args: Vec<Value> = Vec::with_capacity(user_arg_count);
for _ in 0..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 user_args.reverse(); // Now in logical order: arg1..argN
@ -915,7 +943,12 @@ impl VirtualMachine {
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize; as usize;
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| { 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); self.push(val);
} }
@ -944,23 +977,24 @@ impl VirtualMachine {
self.push(Value::Boolean(val != 0)); self.push(Value::Boolean(val != 0));
} }
OpCode::Pop => { OpCode::Pop => {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.pop_trap(opcode, start_pc as u32, "POP requires one operand")?;
} }
OpCode::PopN => { OpCode::PopN => {
let n = instr let n = instr
.imm_u32() .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
for _ in 0..n { 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 => { 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); self.push(val);
} }
OpCode::Swap => { OpCode::Swap => {
let a = 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().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let b = self.pop_trap(opcode, start_pc as u32, "SWAP requires two operands")?;
self.push(a); self.push(a);
self.push(b); self.push(b);
} }
@ -979,7 +1013,7 @@ impl VirtualMachine {
(Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a + *b as f64)), (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::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::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) { 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::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::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::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::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) { 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::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::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::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::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) { OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => { (Value::Int32(a), Value::Int32(b)) => {
@ -1072,7 +1106,7 @@ impl VirtualMachine {
} }
Ok(Value::Float(a / b as f64)) 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) { OpCode::Mod => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => { (Value::Int32(a), Value::Int32(b)) => {
@ -1087,7 +1121,7 @@ impl VirtualMachine {
} }
Ok(Value::Int64(a % b)) 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 => { OpCode::Eq => {
self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))? 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| { OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| {
a.partial_cmp(&b) a.partial_cmp(&b)
.map(|o| Value::Boolean(o == std::cmp::Ordering::Less)) .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| { OpCode::Gt => self.binary_op(opcode, start_pc as u32, |a, b| {
a.partial_cmp(&b) a.partial_cmp(&b)
.map(|o| Value::Boolean(o == std::cmp::Ordering::Greater)) .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| { OpCode::Lte => self.binary_op(opcode, start_pc as u32, |a, b| {
a.partial_cmp(&b) a.partial_cmp(&b)
.map(|o| Value::Boolean(o != std::cmp::Ordering::Greater)) .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| { OpCode::Gte => self.binary_op(opcode, start_pc as u32, |a, b| {
a.partial_cmp(&b) a.partial_cmp(&b)
.map(|o| Value::Boolean(o != std::cmp::Ordering::Less)) .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) { 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)), (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) { 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)), (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 => { 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 { if let Value::Boolean(b) = val {
self.push(Value::Boolean(!b)); self.push(Value::Boolean(!b));
} else { } 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) { 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::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a & b)),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) & 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))), (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) { 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::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a | b)),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(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::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))), (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) { 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::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a ^ b)),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(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::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))), (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) { 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::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))) 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))), (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) { 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::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))) 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))), (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 => { 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 { match val {
Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())), Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())),
Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())), Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())),
Value::Float(a) => self.push(Value::Float(-a)), 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() .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize; 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() { if idx >= self.globals.len() {
self.globals.resize(idx + 1, Value::Int32(0)); self.globals.resize(idx + 1, Value::Int32(0));
} }
@ -1234,7 +1279,8 @@ impl VirtualMachine {
let slot = instr let slot = instr
.imm_u32() .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; .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(|| { let frame = self.call_stack.last().ok_or_else(|| {
LogicalFrameEndingReason::Panic("No active call frame".into()) LogicalFrameEndingReason::Panic("No active call frame".into())
})?; })?;
@ -1268,12 +1314,17 @@ impl VirtualMachine {
})?; })?;
if self.operand_stack.len() < callee.param_slots as usize { if self.operand_stack.len() < callee.param_slots as usize {
return Err(LogicalFrameEndingReason::Panic(format!( return Err(self.trap(
TRAP_STACK_UNDERFLOW,
opcode as u16,
format!(
"Stack underflow during CALL to func {}: expected at least {} arguments, got {}", "Stack underflow during CALL to func {}: expected at least {} arguments, got {}",
func_id, func_id,
callee.param_slots, callee.param_slots,
self.operand_stack.len() self.operand_stack.len()
))); ),
start_pc as u32,
));
} }
let stack_base = self.operand_stack.len() - callee.param_slots as usize; let stack_base = self.operand_stack.len() - callee.param_slots as usize;
@ -1292,7 +1343,12 @@ impl VirtualMachine {
} }
OpCode::Ret => { OpCode::Ret => {
let frame = self.call_stack.pop().ok_or_else(|| { 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 func = &self.program.functions[frame.func_idx];
let return_slots = func.return_slots as usize; let return_slots = func.return_slots as usize;
@ -1649,6 +1705,24 @@ impl VirtualMachine {
self.operand_stack.last().ok_or("Stack underflow".into()) self.operand_stack.last().ok_or("Stack underflow".into())
} }
fn pop_trap<S: Into<String>>(
&mut self,
opcode: OpCode,
pc: u32,
message: S,
) -> Result<Value, LogicalFrameEndingReason> {
self.pop().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc))
}
fn peek_trap<S: Into<String>>(
&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<F>( fn binary_op<F>(
&mut self, &mut self,
opcode: OpCode, opcode: OpCode,
@ -1658,15 +1732,14 @@ impl VirtualMachine {
where where
F: FnOnce(Value, Value) -> Result<Value, OpError>, F: FnOnce(Value, Value) -> Result<Value, OpError>,
{ {
let b = 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().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let a = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?;
match f(a, b) { match f(a, b) {
Ok(res) => { Ok(res) => {
self.push(res); self.push(res);
Ok(()) Ok(())
} }
Err(OpError::Trap(code, msg)) => Err(self.trap(code, opcode as u16, msg, start_pc)), 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 crate::HostReturn;
use prometeu_bytecode::model::{BytecodeModule, SyscallDecl}; use prometeu_bytecode::model::{BytecodeModule, SyscallDecl};
use prometeu_bytecode::{ 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; use prometeu_hal::expect_int;
@ -2128,6 +2202,29 @@ mod tests {
assert_eq!(vm.peek().unwrap(), &Value::String("hello".into())); 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] #[test]
fn test_call_ret_scope_separation() { fn test_call_ret_scope_separation() {
let mut rom = Vec::new(); let mut rom = Vec::new();
@ -2441,11 +2538,40 @@ mod tests {
let mut vm = new_test_vm(rom.clone(), vec![]); let mut vm = new_test_vm(rom.clone(), vec![]);
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); 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.pc, 8); // PushI32 (6 bytes) + Trap (2 bytes)
assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); 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] #[test]
fn test_pop_n_opcode() { fn test_pop_n_opcode() {
let mut native = MockNative; 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] #[test]
fn test_invalid_syscall_trap() { fn test_invalid_syscall_trap() {
let rom = vec![ let rom = vec![

View File

@ -2,6 +2,7 @@ use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware}; use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::debugger_protocol::*; use prometeu_hal::debugger_protocol::*;
use prometeu_system::CrashReport;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream}; use std::net::{TcpListener, TcpStream};
@ -22,6 +23,8 @@ pub struct HostDebugger {
last_log_seq: u64, last_log_seq: u64,
/// Frame tracker to send telemetry snapshots periodically. /// Frame tracker to send telemetry snapshots periodically.
last_telemetry_frame: u64, last_telemetry_frame: u64,
/// Last fault summary sent to the debugger client.
last_fault_summary: Option<String>,
} }
impl HostDebugger { impl HostDebugger {
@ -33,6 +36,7 @@ impl HostDebugger {
stream: None, stream: None,
last_log_seq: 0, last_log_seq: 0,
last_telemetry_frame: 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"); stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
self.stream = Some(stream); self.stream = Some(stream);
self.last_fault_summary = None;
// Immediately send the Handshake message to identify the Runtime and App. // Immediately send the Handshake message to identify the Runtime and App.
let handshake = DebugResponse::Handshake { let handshake = DebugResponse::Handshake {
@ -221,6 +226,12 @@ impl HostDebugger {
/// Scans the system for new information to push to the debugger client. /// Scans the system for new information to push to the debugger client.
fn stream_events(&mut self, firmware: &mut Firmware) { 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. // 1. Process and send new log entries.
let new_events = firmware.os.log_service.get_after(self.last_log_seq); let new_events = firmware.os.log_service.get_after(self.last_log_seq);
for event in new_events { for event in new_events {
@ -295,4 +306,25 @@ impl HostDebugger {
self.last_telemetry_frame = current_frame; 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);
}
} }

View File

@ -207,6 +207,14 @@ impl HostRunner {
self.hardware.gfx.draw_text(10, 90, &msg, color_warn); 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);
}
} }
} }

View File

@ -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.