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"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-drivers",
"prometeu-hal",
"prometeu-system",

View File

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

View File

@ -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};

View File

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

View File

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

View File

@ -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<FirmwareState> {
@ -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"));
}
}

View File

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

View File

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

View File

@ -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<String>,
init_error: Option<CrashReport>,
}
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<FirmwareState> {
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 {

View File

@ -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<u32>,
trap_code: Option<u32>,
opcode: Option<u16>,
},
#[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"));
}
}

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 services;
mod virtual_machine_runtime;
pub use crash_report::CrashReport;
pub use programs::PrometeuHub;
pub use services::fs;
pub use virtual_machine_runtime::VirtualMachineRuntime;

View File

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

View File

@ -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<CrashReport>,
/// 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<String> {
) -> Option<CrashReport> {
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<String> {
) -> Option<CrashReport> {
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<u8>, 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::<Value>::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 { .. })));
}
}

View File

@ -47,11 +47,25 @@ pub struct CoroutineData {
pub struct Heap {
// Tombstone-aware store: Some(obj) = live allocation; None = freed slot.
objects: Vec<Option<StoredObject>>,
// Reclaimed slots available for deterministic reuse (LIFO).
free_list: Vec<usize>,
}
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();

View File

@ -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<TrapInfo> 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<Value> = 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<Value> = 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<Value> = 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<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>(
&mut self,
opcode: OpCode,
@ -1658,15 +1732,14 @@ impl VirtualMachine {
where
F: FnOnce(Value, Value) -> Result<Value, OpError>,
{
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![

View File

@ -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<String>,
}
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);
}
}

View File

@ -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);
}
}
}

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.