implements PR-007 to PR011
This commit is contained in:
parent
c0ed9ba8e0
commit
a75df486ce
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
@ -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" }
|
||||||
|
|||||||
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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) {}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
crates/console/prometeu-system/src/crash_report.rs
Normal file
55
crates/console/prometeu-system/src/crash_report.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 { .. })));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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![
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
Loading…
x
Reference in New Issue
Block a user