From f04c12d78ced07de584e61d37d405c4ee190b172 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Sat, 17 Jan 2026 21:14:36 +0000 Subject: [PATCH] add debugger --- Cargo.lock | 1 + crates/host-desktop/Cargo.toml | 3 +- crates/host-desktop/src/main.rs | 34 ++- crates/host-desktop/src/prometeu_runner.rs | 273 +++++++++++++++++- crates/prometeu-core/src/debugger_protocol.rs | 86 ++++++ .../prometeu-core/src/firmware/boot_target.rs | 2 +- .../src/firmware/firmware_step_launch_hub.rs | 2 +- crates/prometeu-core/src/lib.rs | 1 + .../src/prometeu_os/prometeu_os.rs | 40 +++ .../src/virtual_machine/virtual_machine.rs | 88 +++++- 10 files changed, 506 insertions(+), 24 deletions(-) create mode 100644 crates/prometeu-core/src/debugger_protocol.rs diff --git a/Cargo.lock b/Cargo.lock index 9ce4a4dc..f52d5fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,6 +724,7 @@ dependencies = [ "pixels", "prometeu-core", "ringbuf", + "serde_json", "winit", ] diff --git a/crates/host-desktop/Cargo.toml b/crates/host-desktop/Cargo.toml index edeb0800..9c343f66 100644 --- a/crates/host-desktop/Cargo.toml +++ b/crates/host-desktop/Cargo.toml @@ -8,4 +8,5 @@ prometeu-core = { path = "../prometeu-core" } winit = "0.30.12" pixels = "0.15.0" cpal = "0.15.3" -ringbuf = "0.4.7" \ No newline at end of file +ringbuf = "0.4.7" +serde_json = "1.0" \ No newline at end of file diff --git a/crates/host-desktop/src/main.rs b/crates/host-desktop/src/main.rs index 85861849..69e6a718 100644 --- a/crates/host-desktop/src/main.rs +++ b/crates/host-desktop/src/main.rs @@ -38,26 +38,32 @@ fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); let mut fs_root = None; let mut cap_config = None; - let mut boot_target = BootTarget::Hub; + let mut cartridge_path = None; + let mut debug_mode = false; + let mut debug_port = 7777; let mut i = 1; // Pula o nome do executável while i < args.len() { match args[i].as_str() { "--run" => { if i + 1 < args.len() { - boot_target = BootTarget::Cartridge { - path: args[i + 1].clone(), - debug: false, - }; + cartridge_path = Some(args[i + 1].clone()); + debug_mode = false; i += 1; } } "--debug" => { if i + 1 < args.len() { - boot_target = BootTarget::Cartridge { - path: args[i + 1].clone(), - debug: true, - }; + cartridge_path = Some(args[i + 1].clone()); + debug_mode = true; + i += 1; + } + } + "--port" => { + if i + 1 < args.len() { + if let Ok(port) = args[i + 1].parse::() { + debug_port = port; + } i += 1; } } @@ -78,6 +84,16 @@ fn main() -> Result<(), Box> { i += 1; } + let boot_target = if let Some(path) = cartridge_path { + BootTarget::Cartridge { + path, + debug: debug_mode, + debug_port: if debug_mode { debug_port } else { 7777 }, + } + } else { + BootTarget::Hub + }; + let event_loop = EventLoop::new()?; let mut runner = PrometeuRunner::new(fs_root, cap_config); diff --git a/crates/host-desktop/src/prometeu_runner.rs b/crates/host-desktop/src/prometeu_runner.rs index c5573af6..916e7d99 100644 --- a/crates/host-desktop/src/prometeu_runner.rs +++ b/crates/host-desktop/src/prometeu_runner.rs @@ -8,6 +8,7 @@ use prometeu_core::hardware::{AudioCommand, InputSignals, OUTPUT_SAMPLE_RATE}; use prometeu_core::Hardware; use ringbuf::traits::{Consumer, Producer, Split}; use ringbuf::HeapRb; +use std::net::{TcpListener, TcpStream}; use std::sync::Arc; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -17,8 +18,10 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::keyboard::{KeyCode, PhysicalKey}; use winit::window::{Window, WindowAttributes, WindowId}; -use prometeu_core::model::{CartridgeError, CartridgeLoader}; +use prometeu_core::model::CartridgeLoader; use prometeu_core::telemetry::CertificationConfig; +use prometeu_core::debugger_protocol::*; +use std::io::{Read, Write}; pub struct PrometeuRunner { window: Option<&'static Window>, @@ -43,6 +46,10 @@ pub struct PrometeuRunner { overlay_enabled: bool, debug_waiting_for_start: bool, + debug_listener: Option, + debug_stream: Option, + debug_last_log_seq: u64, + debug_last_telemetry_frame: u64, audio_load_accum_us: u64, audio_load_samples: u64, @@ -55,17 +62,206 @@ pub struct PrometeuRunner { impl PrometeuRunner { pub(crate) fn set_boot_target(&mut self, boot_target: BootTarget) { self.firmware.boot_target = boot_target.clone(); - if let BootTarget::Cartridge { debug: true, .. } = boot_target { + if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target { self.debug_waiting_for_start = true; - println!("[Debugger] Waiting for start command..."); - println!("[Debugger] (Stub: press D to start execution)"); + + // Pré-carrega informações do cartucho para o handshake + if let Ok(cartridge) = CartridgeLoader::load(&path) { + self.firmware.os.initialize_vm(&mut self.firmware.vm, &cartridge); + } + + match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { + Ok(listener) => { + listener.set_nonblocking(true).expect("Cannot set non-blocking"); + self.debug_listener = Some(listener); + println!("[Debugger] Listening for start command on port {}...", debug_port); + } + Err(e) => { + eprintln!("[Debugger] Failed to bind to port {}: {}", debug_port, e); + } + } + + println!("[Debugger] (Or press D to start execution)"); } } - pub(crate) fn load_cartridge(&mut self, path: &str) -> Result<(), CartridgeError> { - let cartridge = CartridgeLoader::load(path)?; - self.firmware.load_cartridge(cartridge); - Ok(()) + + fn send_debug_response(&mut self, resp: DebugResponse) { + if let Some(stream) = &mut self.debug_stream { + if let Ok(json) = serde_json::to_string(&resp) { + let _ = stream.write_all(json.as_bytes()); + let _ = stream.write_all(b"\n"); + } + } + } + + fn send_debug_event(&mut self, event: DebugEvent) { + if let Some(stream) = &mut self.debug_stream { + if let Ok(json) = serde_json::to_string(&event) { + let _ = stream.write_all(json.as_bytes()); + let _ = stream.write_all(b"\n"); + } + } + } + + fn check_debug_commands(&mut self) { + if self.debug_waiting_for_start { + if let Some(listener) = &self.debug_listener { + if let Ok((stream, _addr)) = listener.accept() { + println!("[Debugger] Connection received!"); + stream.set_nonblocking(true).expect("Cannot set non-blocking on stream"); + + self.debug_stream = Some(stream); + + // Enviar Handshake + let handshake = DebugResponse::Handshake { + runtime_version: "0.1".to_string(), + cartridge: HandshakeCartridge { + app_id: self.firmware.os.current_app_id, + title: self.firmware.os.current_cartridge_title.clone(), + app_version: self.firmware.os.current_cartridge_app_version.clone(), + app_mode: self.firmware.os.current_cartridge_app_mode, + }, + }; + self.send_debug_response(handshake); + } + } + } + + if let Some(mut stream) = self.debug_stream.take() { + let mut buf = [0u8; 4096]; + match stream.read(&mut buf) { + Ok(0) => { + println!("[Debugger] Connection closed by remote."); + self.debug_stream = None; + } + Ok(n) => { + let data = &buf[..n]; + // Processar múltiplos comandos se houver \n + let msg = String::from_utf8_lossy(data); + for line in msg.lines() { + if let Ok(cmd) = serde_json::from_str::(line) { + self.handle_debug_command(cmd); + } + } + self.debug_stream = Some(stream); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + self.debug_stream = Some(stream); + } + Err(e) => { + eprintln!("[Debugger] Connection error: {}", e); + self.debug_stream = None; + } + } + } + + // Streaming de eventos + if self.debug_stream.is_some() { + self.stream_debug_events(); + } + } + + fn handle_debug_command(&mut self, cmd: DebugCommand) { + match cmd { + DebugCommand::Ok | DebugCommand::Start => { + if self.debug_waiting_for_start { + println!("[Debugger] Starting execution..."); + self.debug_waiting_for_start = false; + self.debug_listener = None; + } + self.firmware.os.paused = false; + } + DebugCommand::Pause => { + self.firmware.os.paused = true; + } + DebugCommand::Resume => { + self.firmware.os.paused = false; + } + DebugCommand::Step => { + self.firmware.os.paused = true; + // Executa uma instrução imediatamente + let _ = self.firmware.os.debug_step_instruction(&mut self.firmware.vm, &mut self.hardware); + } + DebugCommand::StepFrame => { + self.firmware.os.paused = false; + self.firmware.os.debug_step_request = true; + } + DebugCommand::GetState => { + let stack_top = self.firmware.vm.operand_stack.iter() + .rev().take(10).filter_map(|v| v.as_integer()).collect(); + + let resp = DebugResponse::State { + pc: self.firmware.vm.pc, + stack_top, + frame_index: self.firmware.os.logical_frame_index, + app_id: self.firmware.os.current_app_id, + }; + self.send_debug_response(resp); + } + DebugCommand::SetBreakpoint { pc } => { + self.firmware.vm.breakpoints.insert(pc); + } + DebugCommand::ListBreakpoints => { + let pcs = self.firmware.vm.breakpoints.iter().cloned().collect(); + self.send_debug_response(DebugResponse::Breakpoints { pcs }); + } + DebugCommand::ClearBreakpoint { pc } => { + self.firmware.vm.breakpoints.remove(&pc); + } + } + } + + fn stream_debug_events(&mut self) { + // Logs + let new_events = self.firmware.os.log_service.get_after(self.debug_last_log_seq); + for event in new_events { + self.debug_last_log_seq = event.seq; + + // Verifica se é um breakpoint hit via tag + if event.tag == 0xDEB1 { + self.send_debug_event(DebugEvent::BreakpointHit { + pc: self.firmware.vm.pc, + frame_index: self.firmware.os.logical_frame_index, + }); + } + + // Certificação via Tags 0xCA01-0xCA03 + if event.tag >= 0xCA01 && event.tag <= 0xCA03 { + let rule = match event.tag { + 0xCA01 => "cycles_budget", + 0xCA02 => "max_syscalls", + 0xCA03 => "max_host_cpu_us", + _ => "unknown" + }.to_string(); + + self.send_debug_event(DebugEvent::Cert { + rule, + used: 0, // Simplificado, informações detalhadas estão na msg do log + limit: 0, + frame_index: self.firmware.os.logical_frame_index, + }); + } + + self.send_debug_event(DebugEvent::Log { + level: format!("{:?}", event.level), + source: format!("{:?}", event.source), + msg: event.msg.clone(), + }); + } + + // Telemetria (a cada novo frame) + let current_frame = self.firmware.os.logical_frame_index; + if current_frame > self.debug_last_telemetry_frame { + let tel = &self.firmware.os.telemetry_last; + self.send_debug_event(DebugEvent::Telemetry { + frame_index: tel.frame_index, + vm_steps: tel.vm_steps, + syscalls: tel.syscalls, + cycles: tel.cycles_used, + }); + self.debug_last_telemetry_frame = current_frame; + } } pub(crate) fn new(fs_root: Option, cap_config: Option) -> Self { @@ -94,6 +290,10 @@ impl PrometeuRunner { current_fps: 0.0, overlay_enabled: false, debug_waiting_for_start: false, + debug_listener: None, + debug_stream: None, + debug_last_log_seq: 0, + debug_last_telemetry_frame: 0, audio_load_accum_us: 0, audio_load_samples: 0, audio_perf_consumer: None, @@ -240,6 +440,7 @@ impl ApplicationHandler for PrometeuRunner { if is_down && code == KeyCode::KeyD && self.debug_waiting_for_start { self.debug_waiting_for_start = false; + self.debug_listener = None; // Fecha o socket println!("[Debugger] Execution started!"); } @@ -295,6 +496,8 @@ impl ApplicationHandler for PrometeuRunner { } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + self.check_debug_commands(); + // Atualiza estado do Filesystem no OS (específico do host-desktop) if let Some(root) = &self.fs_root { use prometeu_core::fs::FsState; @@ -454,4 +657,56 @@ fn rgb565_to_rgb888(px: u16) -> (u8, u8, u8) { let b8 = (b5 << 3) | (b5 >> 2); (r8, g8, b8) -} \ No newline at end of file +} +#[cfg(test)] +mod tests { + use super::*; + use prometeu_core::firmware::BootTarget; + + #[test] + fn test_debug_port_opens() { + let mut runner = PrometeuRunner::new(None, None); + let port = 9999; + runner.set_boot_target(BootTarget::Cartridge { + path: "dummy.bin".to_string(), + debug: true, + debug_port: port, + }); + + assert!(runner.debug_waiting_for_start); + assert!(runner.debug_listener.is_some()); + + // Verifica se conseguimos conectar + use std::net::TcpStream; + use std::io::{Write, Read}; + { + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Deve conectar"); + // Pequeno sleep para garantir que o SO processe + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Simula o loop para aceitar a conexão + runner.check_debug_commands(); + assert!(runner.debug_stream.is_some(), "Stream deve ter sido mantido aberto"); + + // Verifica Handshake + let mut buf = [0u8; 2048]; + let n = stream.read(&mut buf).expect("Deve ler handshake"); + let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake deve ser JSON válido"); + assert_eq!(resp["type"], "handshake"); + + // Envia start via JSON + stream.write_all(b"{\"type\":\"start\"}\n").expect("Conexão deve estar aberta para escrita"); + std::thread::sleep(std::time::Duration::from_millis(50)); + + // Processa o comando recebido + runner.check_debug_commands(); + assert!(!runner.debug_waiting_for_start, "Execução deve ter iniciado após comando start"); + assert!(runner.debug_listener.is_none(), "Listener deve ter sido fechado"); + } + + // Agora que o stream saiu do escopo do teste, o runner deve detectar o fechamento no próximo check + std::thread::sleep(std::time::Duration::from_millis(50)); + runner.check_debug_commands(); + assert!(runner.debug_stream.is_none(), "Stream deve ter sido fechado após o cliente desconectar"); + } +} diff --git a/crates/prometeu-core/src/debugger_protocol.rs b/crates/prometeu-core/src/debugger_protocol.rs new file mode 100644 index 00000000..40511763 --- /dev/null +++ b/crates/prometeu-core/src/debugger_protocol.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; +use crate::model::AppMode; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum DebugCommand { + #[serde(rename = "ok")] + Ok, + #[serde(rename = "start")] + Start, + #[serde(rename = "pause")] + Pause, + #[serde(rename = "resume")] + Resume, + #[serde(rename = "step")] + Step, + #[serde(rename = "stepFrame")] + StepFrame, + #[serde(rename = "getState")] + GetState, + #[serde(rename = "setBreakpoint")] + SetBreakpoint { pc: usize }, + #[serde(rename = "listBreakpoints")] + ListBreakpoints, + #[serde(rename = "clearBreakpoint")] + ClearBreakpoint { pc: usize }, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum DebugResponse { + #[serde(rename = "handshake")] + Handshake { + runtime_version: String, + cartridge: HandshakeCartridge, + }, + #[serde(rename = "state")] + State { + pc: usize, + stack_top: Vec, + frame_index: u64, + app_id: u32, + }, + #[serde(rename = "breakpoints")] + Breakpoints { + pcs: Vec, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HandshakeCartridge { + pub app_id: u32, + pub title: String, + pub app_version: String, + pub app_mode: AppMode, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "event")] +pub enum DebugEvent { + #[serde(rename = "breakpointHit")] + BreakpointHit { + pc: usize, + frame_index: u64, + }, + #[serde(rename = "log")] + Log { + level: String, + source: String, + msg: String, + }, + #[serde(rename = "telemetry")] + Telemetry { + frame_index: u64, + vm_steps: u32, + syscalls: u32, + cycles: u64, + }, + #[serde(rename = "cert")] + Cert { + rule: String, + used: u64, + limit: u64, + frame_index: u64, + }, +} diff --git a/crates/prometeu-core/src/firmware/boot_target.rs b/crates/prometeu-core/src/firmware/boot_target.rs index ef215d2f..807c97b3 100644 --- a/crates/prometeu-core/src/firmware/boot_target.rs +++ b/crates/prometeu-core/src/firmware/boot_target.rs @@ -1,7 +1,7 @@ #[derive(Debug, Clone, PartialEq, Eq)] pub enum BootTarget { Hub, - Cartridge { path: String, debug: bool }, + Cartridge { path: String, debug: bool, debug_port: u16 }, } impl Default for BootTarget { diff --git a/crates/prometeu-core/src/firmware/firmware_step_launch_hub.rs b/crates/prometeu-core/src/firmware/firmware_step_launch_hub.rs index 7b920d83..b120c905 100644 --- a/crates/prometeu-core/src/firmware/firmware_step_launch_hub.rs +++ b/crates/prometeu-core/src/firmware/firmware_step_launch_hub.rs @@ -14,7 +14,7 @@ impl LaunchHubStep { } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { - if let BootTarget::Cartridge { path, debug } = ctx.boot_target { + if let BootTarget::Cartridge { path, .. } = ctx.boot_target { match CartridgeLoader::load(path) { Ok(cartridge) => { // No caso de debug, aqui poderíamos pausar, mas o requisito diz diff --git a/crates/prometeu-core/src/lib.rs b/crates/prometeu-core/src/lib.rs index f88ef5ed..fe373146 100644 --- a/crates/prometeu-core/src/lib.rs +++ b/crates/prometeu-core/src/lib.rs @@ -5,6 +5,7 @@ pub mod model; pub mod firmware; pub mod fs; pub mod telemetry; +pub mod debugger_protocol; mod prometeu_os; mod prometeu_hub; diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 2422666c..1ee7eceb 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -32,12 +32,17 @@ pub struct PrometeuOS { // Log Service pub log_service: LogService, pub current_app_id: u32, + pub current_cartridge_title: String, + pub current_cartridge_app_version: String, + pub current_cartridge_app_mode: crate::model::AppMode, pub logs_written_this_frame: HashMap, // Telemetria e Certificação pub telemetry_current: TelemetryFrame, pub telemetry_last: TelemetryFrame, pub certifier: Certifier, + pub paused: bool, + pub debug_step_request: bool, boot_time: Instant, } @@ -66,10 +71,15 @@ impl PrometeuOS { next_handle: 1, log_service: LogService::new(4096), current_app_id: 0, + current_cartridge_title: String::new(), + current_cartridge_app_version: String::new(), + current_cartridge_app_mode: crate::model::AppMode::Game, logs_written_this_frame: HashMap::new(), telemetry_current: TelemetryFrame::default(), telemetry_last: TelemetryFrame::default(), certifier: Certifier::new(cap_config.unwrap_or_default()), + paused: false, + debug_step_request: false, boot_time, }; @@ -131,6 +141,21 @@ impl PrometeuOS { // Determina o app_id numérico self.current_app_id = cartridge.app_id; + self.current_cartridge_title = cartridge.title.clone(); + self.current_cartridge_app_version = cartridge.app_version.clone(); + self.current_cartridge_app_mode = cartridge.app_mode; + } + + /// Executa uma única instrução da VM (Debug). + pub fn debug_step_instruction(&mut self, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Option { + match vm.step(self, hw) { + 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) + } + } } /// Executa um tick do host (60Hz). @@ -138,6 +163,10 @@ impl PrometeuOS { let start = std::time::Instant::now(); self.tick_index += 1; + if self.paused && !self.debug_step_request { + return None; + } + self.update_fs(); if !self.logical_frame_active { @@ -167,6 +196,12 @@ impl PrometeuOS { self.telemetry_current.cycles_used += run.cycles_used; self.telemetry_current.vm_steps += run.steps_executed; + if run.reason == crate::virtual_machine::LogicalFrameEndingReason::Breakpoint { + self.paused = true; + self.debug_step_request = false; + self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc)); + } + if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync { hw.gfx_mut().render_all(); @@ -183,6 +218,11 @@ impl PrometeuOS { self.logical_frame_index += 1; self.logical_frame_active = false; self.logical_frame_remaining_cycles = 0; + + if self.debug_step_request { + self.paused = true; + self.debug_step_request = false; + } } } Err(e) => { diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 726283d3..c9a0bd3c 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -11,6 +11,7 @@ pub enum LogicalFrameEndingReason { BudgetExhausted, Halted, EndOfRom, + Breakpoint, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -29,6 +30,7 @@ pub struct VirtualMachine { pub heap: Vec, // Simplificado para demo, futuramente RAM/Heap real pub cycles: u64, pub halted: bool, + pub breakpoints: std::collections::HashSet, } impl VirtualMachine { @@ -42,13 +44,21 @@ impl VirtualMachine { heap: Vec::new(), cycles: 0, halted: false, + breakpoints: std::collections::HashSet::new(), } } pub fn initialize(&mut self, program_bytes: Vec, entrypoint: &str) { - // Por enquanto, tratamos os bytes como a ROM diretamente. - // TODO: Implementar parser de .pbc para extrair constant_pool e rom reais. - self.program = Program::new(program_bytes, vec![]); + if program_bytes.starts_with(b"PPBC") { + if let Ok((rom, cp)) = self.parse_pbc(&program_bytes) { + self.program = Program::new(rom, cp); + } else { + self.program = Program::new(program_bytes, vec![]); + } + } else { + // Por enquanto, tratamos os bytes como a ROM diretamente. + self.program = Program::new(program_bytes, vec![]); + } // Se o entrypoint for numérico, podemos tentar usá-lo como PC inicial. // Se não, por enquanto ignoramos ou começamos do 0. @@ -65,6 +75,73 @@ impl VirtualMachine { self.cycles = 0; self.halted = false; } + + fn parse_pbc(&self, bytes: &[u8]) -> Result<(Vec, Vec), String> { + let mut cursor = 4; // Skip "PPBC" + + let cp_count = self.read_u32_at(bytes, &mut cursor)? as usize; + let mut cp = Vec::with_capacity(cp_count); + + for _ in 0..cp_count { + if cursor >= bytes.len() { return Err("Unexpected end of PBC".into()); } + let tag = bytes[cursor]; + cursor += 1; + + match tag { + 1 => { // Integer + let val = self.read_i64_at(bytes, &mut cursor)?; + cp.push(Value::Integer(val)); + } + 2 => { // Float + let val = self.read_f64_at(bytes, &mut cursor)?; + cp.push(Value::Float(val)); + } + 3 => { // Boolean + if cursor >= bytes.len() { return Err("Unexpected end of PBC".into()); } + let val = bytes[cursor] != 0; + cursor += 1; + cp.push(Value::Boolean(val)); + } + 4 => { // String + let len = self.read_u32_at(bytes, &mut cursor)? as usize; + if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); } + let s = String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned(); + cursor += len; + cp.push(Value::String(s)); + } + _ => cp.push(Value::Null), + } + } + + let rom_size = self.read_u32_at(bytes, &mut cursor)? as usize; + if cursor + rom_size > bytes.len() { + return Err("Invalid ROM size in PBC".into()); + } + let rom = bytes[cursor..cursor + rom_size].to_vec(); + + Ok((rom, cp)) + } + + fn read_u32_at(&self, bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor + 4 > bytes.len() { return Err("Unexpected end of PBC".into()); } + let val = u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().unwrap()); + *cursor += 4; + Ok(val) + } + + fn read_i64_at(&self, bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor + 8 > bytes.len() { return Err("Unexpected end of PBC".into()); } + let val = i64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().unwrap()); + *cursor += 8; + Ok(val) + } + + fn read_f64_at(&self, bytes: &[u8], cursor: &mut usize) -> Result { + if *cursor + 8 > bytes.len() { return Err("Unexpected end of PBC".into()); } + let val = f64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().unwrap()); + *cursor += 8; + Ok(val) + } } impl Default for VirtualMachine { @@ -88,6 +165,11 @@ impl VirtualMachine { && !self.halted && self.pc < self.program.rom.len() { + if steps_executed > 0 && self.breakpoints.contains(&self.pc) { + ending_reason = Some(LogicalFrameEndingReason::Breakpoint); + break; + } + let pc_before = self.pc; let cycles_before = self.cycles;