diff --git a/crates/prometeu-bytecode/src/abi.rs b/crates/prometeu-bytecode/src/abi.rs index 1dcf0157..95cae37c 100644 --- a/crates/prometeu-bytecode/src/abi.rs +++ b/crates/prometeu-bytecode/src/abi.rs @@ -36,6 +36,10 @@ pub const TRAP_DEAD_GATE: u32 = 0x02; pub const TRAP_OOB: u32 = 0x03; /// Attempted a typed operation on a gate whose storage type does not match. pub const TRAP_TYPE: u32 = 0x04; +/// The syscall ID provided is not recognized by the system. +pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007; +/// Not enough arguments on the stack for the requested syscall. +pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008; /// Detailed information about a runtime trap. #[derive(Debug, Clone, PartialEq, Eq)] @@ -74,6 +78,8 @@ mod tests { assert_eq!(TRAP_DEAD_GATE, 0x02); assert_eq!(TRAP_OOB, 0x03); assert_eq!(TRAP_TYPE, 0x04); + assert_eq!(TRAP_INVALID_SYSCALL, 0x07); + assert_eq!(TRAP_STACK_UNDERFLOW, 0x08); } #[test] @@ -86,6 +92,10 @@ HIP Traps: - OOB (0x03): Access beyond allocated slots. - TYPE (0x04): Type mismatch during heap access. +System Traps: +- INVALID_SYSCALL (0x07): Unknown syscall ID. +- STACK_UNDERFLOW (0x08): Missing syscall arguments. + Operand Sizes: - Alloc: 8 bytes (u32 type_id, u32 slots) - GateLoad: 4 bytes (u32 offset) @@ -95,8 +105,9 @@ Operand Sizes: // This test serves as a "doc-lock". // If you change the ABI, you must update this string. let current_info = format!( - "\nHIP Traps:\n- INVALID_GATE (0x{:02X}): Non-existent gate handle.\n- DEAD_GATE (0x{:02X}): Gate handle with RC=0.\n- OOB (0x{:02X}): Access beyond allocated slots.\n- TYPE (0x{:02X}): Type mismatch during heap access.\n\nOperand Sizes:\n- Alloc: {} bytes (u32 type_id, u32 slots)\n- GateLoad: {} bytes (u32 offset)\n- GateStore: {} bytes (u32 offset)\n- PopN: {} bytes (u32 count)\n", + "\nHIP Traps:\n- INVALID_GATE (0x{:02X}): Non-existent gate handle.\n- DEAD_GATE (0x{:02X}): Gate handle with RC=0.\n- OOB (0x{:02X}): Access beyond allocated slots.\n- TYPE (0x{:02X}): Type mismatch during heap access.\n\nSystem Traps:\n- INVALID_SYSCALL (0x{:02X}): Unknown syscall ID.\n- STACK_UNDERFLOW (0x{:02X}): Missing syscall arguments.\n\nOperand Sizes:\n- Alloc: {} bytes (u32 type_id, u32 slots)\n- GateLoad: {} bytes (u32 offset)\n- GateStore: {} bytes (u32 offset)\n- PopN: {} bytes (u32 count)\n", TRAP_INVALID_GATE, TRAP_DEAD_GATE, TRAP_OOB, TRAP_TYPE, + TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW, operand_size(OpCode::Alloc), operand_size(OpCode::GateLoad), operand_size(OpCode::GateStore), diff --git a/crates/prometeu-core/src/hardware/syscalls.rs b/crates/prometeu-core/src/hardware/syscalls.rs index 67bf39dd..0e91970e 100644 --- a/crates/prometeu-core/src/hardware/syscalls.rs +++ b/crates/prometeu-core/src/hardware/syscalls.rs @@ -149,4 +149,46 @@ impl Syscall { _ => None, } } + + pub fn args_count(&self) -> usize { + match self { + Self::SystemHasCart => 0, + Self::SystemRunCart => 0, + Self::GfxClear => 1, + Self::GfxFillRect => 5, + Self::GfxDrawLine => 5, + Self::GfxDrawCircle => 4, + Self::GfxDrawDisc => 5, + Self::GfxDrawSquare => 6, + Self::GfxSetSprite => 10, + Self::GfxDrawText => 4, + Self::InputGetPad => 1, + Self::InputGetPadPressed => 1, + Self::InputGetPadReleased => 1, + Self::InputGetPadHold => 1, + Self::TouchGetX => 0, + Self::TouchGetY => 0, + Self::TouchIsDown => 0, + Self::TouchIsPressed => 0, + Self::TouchIsReleased => 0, + Self::TouchGetHold => 0, + Self::AudioPlaySample => 5, + Self::AudioPlay => 7, + Self::FsOpen => 1, + Self::FsRead => 1, + Self::FsWrite => 2, + Self::FsClose => 1, + Self::FsListDir => 1, + Self::FsExists => 1, + Self::FsDelete => 1, + Self::LogWrite => 2, + Self::LogWriteTag => 3, + Self::AssetLoad => 3, + Self::AssetStatus => 1, + Self::AssetCommit => 1, + Self::AssetCancel => 1, + Self::BankInfo => 1, + Self::BankSlotInfo => 2, + } + } } diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 6da876e1..31482f41 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -5,7 +5,7 @@ use crate::log::{LogLevel, LogService, LogSource}; use crate::model::{BankType, Cartridge, Color}; use crate::prometeu_os::NativeInterface; use crate::telemetry::{CertificationConfig, Certifier, TelemetryFrame}; -use crate::virtual_machine::{Value, VirtualMachine}; +use crate::virtual_machine::{Value, VirtualMachine, HostReturn, SyscallId, VmFault, expect_int, expect_bool}; use std::collections::HashMap; use std::time::Instant; @@ -324,14 +324,14 @@ impl PrometeuOS { // Helper para syscalls - fn syscall_log_write(&mut self, vm: &mut VirtualMachine, level_val: i64, tag: u16, msg: String) -> Result { + fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> { let level = match level_val { 0 => LogLevel::Trace, 1 => LogLevel::Debug, 2 => LogLevel::Info, 3 => LogLevel::Warn, 4 => LogLevel::Error, - _ => return Err(format!("Invalid log level: {}", level_val)), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Invalid log level: {}", level_val))), }; let app_id = self.current_app_id; @@ -342,8 +342,7 @@ impl PrometeuOS { self.logs_written_this_frame.insert(app_id, count + 1); self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string()); } - vm.push(Value::Null); - return Ok(50); + return Ok(()); } self.logs_written_this_frame.insert(app_id, count + 1); @@ -355,8 +354,7 @@ impl PrometeuOS { self.log(level, LogSource::App { app_id }, tag, final_msg); - vm.push(Value::Null); - Ok(100) + Ok(()) } pub fn get_color(&self, value: i64) -> Color { @@ -411,6 +409,17 @@ mod tests { use crate::virtual_machine::{Value, VirtualMachine}; use crate::Hardware; + fn call_syscall(os: &mut PrometeuOS, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { + let args_count = Syscall::from_u32(id).expect(&format!("Invalid syscall id: 0x{:08X}", id)).args_count(); + let mut args = Vec::new(); + for _ in 0..args_count { + args.push(vm.pop().unwrap()); + } + args.reverse(); + let mut ret = HostReturn::new(&mut vm.operand_stack); + os.syscall(id, &args, &mut ret, hw) + } + #[test] fn test_infinite_loop_budget_reset_bug() { let mut os = PrometeuOS::new(None); @@ -542,7 +551,7 @@ mod tests { vm.push(Value::Boolean(false)); // arg9: flipY vm.push(Value::Int32(4)); // arg10: priority - let res = os.syscall(0x1007, &mut vm, &mut hw); + let res = call_syscall(&mut os, 0x1007, &mut vm, &mut hw); assert!(res.is_ok(), "GfxSetSprite syscall should succeed, but got: {:?}", res.err()); } @@ -564,9 +573,13 @@ mod tests { vm.push(Value::Int32(0)); vm.push(Value::String("mouse_cursor".to_string())); // arg1? - let res = os.syscall(0x1007, &mut vm, &mut hw); + let res = call_syscall(&mut os, 0x1007, &mut vm, &mut hw); assert!(res.is_err()); - assert_eq!(res.err().unwrap(), "Expected integer"); // Because it tries to pop priority but gets a string + // Because it tries to pop priority but gets a string + match res.err().unwrap() { + VmFault::Trap(code, _) => assert_eq!(code, prometeu_bytecode::abi::TRAP_TYPE), + _ => panic!("Expected Trap"), + } } #[test] @@ -580,7 +593,7 @@ mod tests { // 1. Normal log test vm.push(Value::Int64(2)); // Info vm.push(Value::String("Hello Log".to_string())); - let res = os.syscall(0x5001, &mut vm, &mut hw); + let res = call_syscall(&mut os, 0x5001, &mut vm, &mut hw); assert!(res.is_ok()); let recent = os.log_service.get_recent(1); @@ -592,7 +605,7 @@ mod tests { let long_msg = "A".repeat(300); vm.push(Value::Int64(3)); // Warn vm.push(Value::String(long_msg)); - os.syscall(0x5001, &mut vm, &mut hw).unwrap(); + call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap(); let recent = os.log_service.get_recent(1); assert_eq!(recent[0].msg.len(), 256); @@ -603,13 +616,13 @@ mod tests { for i in 0..8 { vm.push(Value::Int64(2)); vm.push(Value::String(format!("Log {}", i))); - os.syscall(0x5001, &mut vm, &mut hw).unwrap(); + call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap(); } // The 11th log should be ignored (and generate a system warning) vm.push(Value::Int64(2)); vm.push(Value::String("Eleventh log".to_string())); - os.syscall(0x5001, &mut vm, &mut hw).unwrap(); + call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap(); let recent = os.log_service.get_recent(2); // The last log should be the rate limit warning (came after the 10th log attempted) @@ -622,7 +635,7 @@ mod tests { os.begin_logical_frame(&InputSignals::default(), &mut hw); vm.push(Value::Int64(2)); vm.push(Value::String("New frame log".to_string())); - os.syscall(0x5001, &mut vm, &mut hw).unwrap(); + call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap(); let recent = os.log_service.get_recent(1); assert_eq!(recent[0].msg, "New frame log"); @@ -631,7 +644,7 @@ mod tests { vm.push(Value::Int64(2)); // Info vm.push(Value::Int64(42)); // Tag vm.push(Value::String("Tagged Log".to_string())); - os.syscall(0x5002, &mut vm, &mut hw).unwrap(); + call_syscall(&mut os, 0x5002, &mut vm, &mut hw).unwrap(); let recent = os.log_service.get_recent(1); assert_eq!(recent[0].msg, "Tagged Log"); @@ -640,7 +653,7 @@ mod tests { // 6. GFX Syscall return test vm.push(Value::Int64(1)); // color_idx - os.syscall(0x1001, &mut vm, &mut hw).unwrap(); // gfx.clear + call_syscall(&mut os, 0x1001, &mut vm, &mut hw).unwrap(); // gfx.clear assert_eq!(vm.pop().unwrap(), Value::Null); } @@ -682,6 +695,21 @@ mod tests { assert!(!os.logical_frame_active); assert!(vm.call_stack.is_empty()); } + + #[test] + fn test_os_unknown_syscall_returns_trap() { + let mut os = PrometeuOS::new(None); + let mut vm = VirtualMachine::default(); + let mut hw = crate::Hardware::new(); + let mut ret = HostReturn::new(&mut vm.operand_stack); + + let res = os.syscall(0xDEADBEEF, &[], &mut ret, &mut hw); + assert!(res.is_err()); + match res.err().unwrap() { + VmFault::Trap(code, _) => assert_eq!(code, prometeu_bytecode::abi::TRAP_INVALID_SYSCALL), + _ => panic!("Expected Trap"), + } + } } impl NativeInterface for PrometeuOS { @@ -696,113 +724,112 @@ impl NativeInterface for PrometeuOS { /// - 0x5000: Logging /// /// Each syscall returns the number of virtual cycles it consumed. - fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result { + fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { self.telemetry_current.syscalls += 1; - let syscall = Syscall::from_u32(id).ok_or_else(|| format!("Unknown syscall: 0x{:08X}", id))?; + let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_INVALID_SYSCALL, format!( + "Unknown syscall: 0x{:08X}", id + )))?; match syscall { // --- System Syscalls --- // system.has_cart() -> bool Syscall::SystemHasCart => { - // Returns true if a cartridge is available. - vm.push(Value::Boolean(true)); // For now, assume true or check state - Ok(10) + ret.push_bool(true); + Ok(()) } // system.run_cart() -> null Syscall::SystemRunCart => { - // Triggers loading and execution of the current cartridge. - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } // --- GFX Syscalls --- // gfx.clear(color_index) -> null Syscall::GfxClear => { - let color_val = vm.pop_integer()?; + let color_val = expect_int(args, 0)?; let color = self.get_color(color_val); hw.gfx_mut().clear(color); - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } // gfx.draw_rect(x, y, w, h, color_index) -> null Syscall::GfxFillRect => { - let color_val = vm.pop_integer()?; - let h = vm.pop_integer()? as i32; - let w = vm.pop_integer()? as i32; - let y = vm.pop_integer()? as i32; - let x = vm.pop_integer()? as i32; + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let w = expect_int(args, 2)? as i32; + let h = expect_int(args, 3)? as i32; + let color_val = expect_int(args, 4)?; let color = self.get_color(color_val); hw.gfx_mut().fill_rect(x, y, w, h, color); - vm.push(Value::Null); - Ok(200) + ret.push_null(); + Ok(()) } // gfx.draw_line(x1, y1, x2, y2, color_index) -> null Syscall::GfxDrawLine => { - let color_val = vm.pop_integer()?; - let y2 = vm.pop_integer()? as i32; - let x2 = vm.pop_integer()? as i32; - let y1 = vm.pop_integer()? as i32; - let x1 = vm.pop_integer()? as i32; + let x1 = expect_int(args, 0)? as i32; + let y1 = expect_int(args, 1)? as i32; + let x2 = expect_int(args, 2)? as i32; + let y2 = expect_int(args, 3)? as i32; + let color_val = expect_int(args, 4)?; let color = self.get_color(color_val); hw.gfx_mut().draw_line(x1, y1, x2, y2, color); - vm.push(Value::Null); - Ok(200) + ret.push_null(); + Ok(()) } // gfx.draw_circle(x, y, r, color_index) -> null Syscall::GfxDrawCircle => { - let color_val = vm.pop_integer()?; - let r = vm.pop_integer()? as i32; - let y = vm.pop_integer()? as i32; - let x = vm.pop_integer()? as i32; + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let r = expect_int(args, 2)? as i32; + let color_val = expect_int(args, 3)?; let color = self.get_color(color_val); hw.gfx_mut().draw_circle(x, y, r, color); - vm.push(Value::Null); - Ok(200) + ret.push_null(); + Ok(()) } // gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) -> null Syscall::GfxDrawDisc => { - let fill_color_val = vm.pop_integer()?; - let border_color_val = vm.pop_integer()?; - let r = vm.pop_integer()? as i32; - let y = vm.pop_integer()? as i32; - let x = vm.pop_integer()? as i32; + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let r = expect_int(args, 2)? as i32; + let border_color_val = expect_int(args, 3)?; + let fill_color_val = expect_int(args, 4)?; let fill_color = self.get_color(fill_color_val); let border_color = self.get_color(border_color_val); hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); - vm.push(Value::Null); - Ok(300) + ret.push_null(); + Ok(()) } // gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) -> null Syscall::GfxDrawSquare => { - let fill_color_val = vm.pop_integer()?; - let border_color_val = vm.pop_integer()?; - let h = vm.pop_integer()? as i32; - let w = vm.pop_integer()? as i32; - let y = vm.pop_integer()? as i32; - let x = vm.pop_integer()? as i32; + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let w = expect_int(args, 2)? as i32; + let h = expect_int(args, 3)? as i32; + let border_color_val = expect_int(args, 4)?; + let fill_color_val = expect_int(args, 5)?; let fill_color = self.get_color(fill_color_val); let border_color = self.get_color(border_color_val); hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); - vm.push(Value::Null); - Ok(200) + ret.push_null(); + Ok(()) } // gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority) Syscall::GfxSetSprite => { - let priority = vm.pop_integer()? as u8; - let flip_y = vm.pop_integer()? != 0; - let flip_x = vm.pop_integer()? != 0; - let active = vm.pop_integer()? != 0; - let palette_id = vm.pop_integer()? as u8; - let tile_id = vm.pop_integer()? as u16; - let y = vm.pop_integer()? as i32; - let x = vm.pop_integer()? as i32; - let index = vm.pop_integer()? as usize; - let val = vm.pop()?; - let asset_name = match val { - Value::String(ref s) => s.clone(), - _ => return Err(format!("Expected string asset_name in GfxSetSprite, but got {:?}", val).into()), + let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())), }; + let index = expect_int(args, 1)? as usize; + let x = expect_int(args, 2)? as i32; + let y = expect_int(args, 3)? as i32; + let tile_id = expect_int(args, 4)? as u16; + let palette_id = expect_int(args, 5)? as u8; + let active = expect_bool(args, 6)?; + let flip_x = expect_bool(args, 7)?; + let flip_y = expect_bool(args, 8)?; + let priority = expect_int(args, 9)? as u8; let bank_id = hw.assets().find_slot_by_name(&asset_name, crate::model::BankType::TILES).unwrap_or(0); @@ -818,265 +845,267 @@ impl NativeInterface for PrometeuOS { priority, }; } - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } Syscall::GfxDrawText => { - let color_val = vm.pop_integer()?; - let color = self.get_color(color_val); - let msg = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string message".into()), + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())), }; - let y = vm.pop_integer()? as i32; - let x = vm.pop_integer()? as i32; + let color_val = expect_int(args, 3)?; + let color = self.get_color(color_val); hw.gfx_mut().draw_text(x, y, &msg, color); - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } // --- Input Syscalls --- // input.get_pad(button_id) -> bool Syscall::InputGetPad => { - let button_id = vm.pop_integer()? as u32; + let button_id = expect_int(args, 0)? as u32; let is_down = self.is_button_down(button_id, hw); - vm.push(Value::Boolean(is_down)); - Ok(50) + ret.push_bool(is_down); + Ok(()) } Syscall::InputGetPadPressed => { - let button_id = vm.pop_integer()? as u32; + let button_id = expect_int(args, 0)? as u32; let val = self.get_button(button_id, hw).map(|b| b.pressed).unwrap_or(false); - vm.push(Value::Boolean(val)); - Ok(50) + ret.push_bool(val); + Ok(()) } Syscall::InputGetPadReleased => { - let button_id = vm.pop_integer()? as u32; + let button_id = expect_int(args, 0)? as u32; let val = self.get_button(button_id, hw).map(|b| b.released).unwrap_or(false); - vm.push(Value::Boolean(val)); - Ok(50) + ret.push_bool(val); + Ok(()) } Syscall::InputGetPadHold => { - let button_id = vm.pop_integer()? as u32; + let button_id = expect_int(args, 0)? as u32; let val = self.get_button(button_id, hw).map(|b| b.hold_frames).unwrap_or(0); - vm.push(Value::Int32(val as i32)); - Ok(50) + ret.push_int(val as i64); + Ok(()) } Syscall::TouchGetX => { - vm.push(Value::Int32(hw.touch().x)); - Ok(50) + ret.push_int(hw.touch().x as i64); + Ok(()) } Syscall::TouchGetY => { - vm.push(Value::Int32(hw.touch().y)); - Ok(50) + ret.push_int(hw.touch().y as i64); + Ok(()) } Syscall::TouchIsDown => { - vm.push(Value::Boolean(hw.touch().f.down)); - Ok(50) + ret.push_bool(hw.touch().f.down); + Ok(()) } Syscall::TouchIsPressed => { - vm.push(Value::Boolean(hw.touch().f.pressed)); - Ok(50) + ret.push_bool(hw.touch().f.pressed); + Ok(()) } Syscall::TouchIsReleased => { - vm.push(Value::Boolean(hw.touch().f.released)); - Ok(50) + ret.push_bool(hw.touch().f.released); + Ok(()) } Syscall::TouchGetHold => { - vm.push(Value::Int32(hw.touch().f.hold_frames as i32)); - Ok(50) + ret.push_int(hw.touch().f.hold_frames as i64); + Ok(()) } // --- Audio Syscalls --- // audio.play_sample(sample_id, voice_id, volume, pan, pitch) Syscall::AudioPlaySample => { - let pitch = vm.pop_number()?; - let pan = vm.pop_integer()? as u8; - let volume = vm.pop_integer()? as u8; - let voice_id = vm.pop_integer()? as usize; - let sample_id = vm.pop_integer()? as u32; + let sample_id = expect_int(args, 0)? as u32; + let voice_id = expect_int(args, 1)? as usize; + let volume = expect_int(args, 2)? as u8; + let pan = expect_int(args, 3)? as u8; + let pitch = match args.get(4).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? { + Value::Float(f) => *f, + Value::Int32(i) => *i as f64, + Value::Int64(i) => *i as f64, + Value::Bounded(b) => *b as f64, + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected number for pitch".into())), + }; hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, crate::hardware::LoopMode::Off); - vm.push(Value::Null); - Ok(300) + ret.push_null(); + Ok(()) } // audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode) Syscall::AudioPlay => { - let loop_mode = match vm.pop_integer()? { + let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())), + }; + let sample_id = expect_int(args, 1)? as u16; + let voice_id = expect_int(args, 2)? as usize; + let volume = expect_int(args, 3)? as u8; + let pan = expect_int(args, 4)? as u8; + let pitch = match args.get(5).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? { + Value::Float(f) => *f, + Value::Int32(i) => *i as f64, + Value::Int64(i) => *i as f64, + Value::Bounded(b) => *b as f64, + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected number for pitch".into())), + }; + let loop_mode = match expect_int(args, 6)? { 0 => crate::hardware::LoopMode::Off, _ => crate::hardware::LoopMode::On, }; - let pitch = vm.pop_number()?; - let pan = vm.pop_integer()? as u8; - let volume = vm.pop_integer()? as u8; - let voice_id = vm.pop_integer()? as usize; - let sample_id = vm.pop_integer()? as u16; - let val = vm.pop()?; - let asset_name = match val { - Value::String(ref s) => s.clone(), - _ => return Err(format!("Expected string asset_name in AudioPlay, but got {:?}", val).into()), - }; let bank_id = hw.assets().find_slot_by_name(&asset_name, crate::model::BankType::SOUNDS).unwrap_or(0); hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode); - vm.push(Value::Null); - Ok(300) + ret.push_null(); + Ok(()) } // --- Filesystem Syscalls (0x4000) --- // FS_OPEN(path) -> handle - // Opens a file in the virtual sandbox and returns a numeric handle. Syscall::FsOpen => { - let path = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string path".into()), + let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())), }; if self.fs_state != FsState::Mounted { - vm.push(Value::Int64(-1)); - return Ok(100); + ret.push_int(-1); + return Ok(()); } let handle = self.next_handle; self.open_files.insert(handle, path); self.next_handle += 1; - vm.push(Value::Int64(handle as i64)); - Ok(200) + ret.push_int(handle as i64); + Ok(()) } // FS_READ(handle) -> content Syscall::FsRead => { - let handle = vm.pop_integer()? as u32; - let path = self.open_files.get(&handle).ok_or("Invalid handle")?; + let handle = expect_int(args, 0)? as u32; + let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; match self.fs.read_file(path) { Ok(data) => { let s = String::from_utf8_lossy(&data).into_owned(); - vm.push(Value::String(s)); - Ok(1000) - } - Err(_e) => { - vm.push(Value::Null); - Ok(100) + ret.push_string(s); } + Err(_) => ret.push_null(), } + Ok(()) } // FS_WRITE(handle, content) Syscall::FsWrite => { - let content = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string content".into()), + let handle = expect_int(args, 0)? as u32; + let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? { + Value::String(s) => s.as_bytes().to_vec(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string content".into())), }; - let handle = vm.pop_integer()? as u32; - let path = self.open_files.get(&handle).ok_or("Invalid handle")?; - match self.fs.write_file(path, content.as_bytes()) { - Ok(_) => { - vm.push(Value::Boolean(true)); - Ok(1000) - } - Err(_) => { - vm.push(Value::Boolean(false)); - Ok(100) - } + let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; + match self.fs.write_file(path, &content) { + Ok(_) => ret.push_bool(true), + Err(_) => ret.push_bool(false), } + Ok(()) } // FS_CLOSE(handle) Syscall::FsClose => { - let handle = vm.pop_integer()? as u32; + let handle = expect_int(args, 0)? as u32; self.open_files.remove(&handle); - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } - // FS_LISTDIR(path) + // FS_LIST_DIR(path) Syscall::FsListDir => { - let path = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string path".into()), + let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())), }; match self.fs.list_dir(&path) { Ok(entries) => { - // Returns a string separated by ';' for simple parsing in PVM. let names: Vec = entries.into_iter().map(|e| e.name).collect(); - vm.push(Value::String(names.join(";"))); - Ok(500) - } - Err(_) => { - vm.push(Value::Null); - Ok(100) + ret.push_string(names.join(";")); } + Err(_) => ret.push_null(), } + Ok(()) } - // FS_EXISTS(path) -> bool + // FS_EXISTS(path) Syscall::FsExists => { - let path = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string path".into()), + let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())), }; - vm.push(Value::Boolean(self.fs.exists(&path))); - Ok(100) + ret.push_bool(self.fs.exists(&path)); + Ok(()) } // FS_DELETE(path) Syscall::FsDelete => { - let path = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string path".into()), + let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())), }; match self.fs.delete(&path) { - Ok(_) => vm.push(Value::Boolean(true)), - Err(_) => vm.push(Value::Boolean(false)), + Ok(_) => ret.push_bool(true), + Err(_) => ret.push_bool(false), } - Ok(500) + Ok(()) } // --- Log Syscalls (0x5000) --- // LOG_WRITE(level, msg) Syscall::LogWrite => { - let msg = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string message".into()), + let level = expect_int(args, 0)?; + let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())), }; - let level = vm.pop_integer()?; - self.syscall_log_write(vm, level, 0, msg) + self.syscall_log_write(level, 0, msg)?; + ret.push_null(); + Ok(()) } // LOG_WRITE_TAG(level, tag, msg) Syscall::LogWriteTag => { - let msg = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string message".into()), + let level = expect_int(args, 0)?; + let tag = expect_int(args, 1)? as u16; + let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())), }; - let tag = vm.pop_integer()? as u16; - let level = vm.pop_integer()?; - self.syscall_log_write(vm, level, tag, msg) + self.syscall_log_write(level, tag, msg)?; + ret.push_null(); + Ok(()) } // --- Asset Syscalls --- Syscall::AssetLoad => { - let asset_id = match vm.pop()? { - Value::String(s) => s, - _ => return Err("Expected string asset_id".into()), + let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_id".into())), }; - let asset_type_val = vm.pop_integer()? as u32; - let slot_index = vm.pop_integer()? as usize; + let asset_type_val = expect_int(args, 1)? as u32; + let slot_index = expect_int(args, 2)? as usize; let asset_type = match asset_type_val { 0 => crate::model::BankType::TILES, 1 => crate::model::BankType::SOUNDS, - _ => return Err("Invalid asset type".to_string()), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())), }; let slot = crate::model::SlotRef { asset_type, index: slot_index }; match hw.assets().load(&asset_id, slot) { Ok(handle) => { - vm.push(Value::Int64(handle as i64)); - Ok(1000) + ret.push_int(handle as i64); + Ok(()) } - Err(e) => Err(e), + Err(e) => Err(VmFault::Panic(e)), } } Syscall::AssetStatus => { - let handle = vm.pop_integer()? as u32; + let handle = expect_int(args, 0)? as u32; let status = hw.assets().status(handle); let status_val = match status { crate::model::LoadStatus::PENDING => 0, @@ -1086,46 +1115,46 @@ impl NativeInterface for PrometeuOS { crate::model::LoadStatus::CANCELED => 4, crate::model::LoadStatus::ERROR => 5, }; - vm.push(Value::Int64(status_val)); - Ok(100) + ret.push_int(status_val); + Ok(()) } Syscall::AssetCommit => { - let handle = vm.pop_integer()? as u32; + let handle = expect_int(args, 0)? as u32; hw.assets().commit(handle); - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } Syscall::AssetCancel => { - let handle = vm.pop_integer()? as u32; + let handle = expect_int(args, 0)? as u32; hw.assets().cancel(handle); - vm.push(Value::Null); - Ok(100) + ret.push_null(); + Ok(()) } Syscall::BankInfo => { - let asset_type_val = vm.pop_integer()? as u32; + let asset_type_val = expect_int(args, 0)? as u32; let asset_type = match asset_type_val { 0 => crate::model::BankType::TILES, 1 => crate::model::BankType::SOUNDS, - _ => return Err("Invalid asset type".to_string()), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())), }; let info = hw.assets().bank_info(asset_type); let json = serde_json::to_string(&info).unwrap_or_default(); - vm.push(Value::String(json)); - Ok(500) + ret.push_string(json); + Ok(()) } Syscall::BankSlotInfo => { - let slot_index = vm.pop_integer()? as usize; - let asset_type_val = vm.pop_integer()? as u32; + let asset_type_val = expect_int(args, 0)? as u32; + let slot_index = expect_int(args, 1)? as usize; let asset_type = match asset_type_val { 0 => crate::model::BankType::TILES, 1 => crate::model::BankType::SOUNDS, - _ => return Err("Invalid asset type".to_string()), + _ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())), }; let slot = crate::model::SlotRef { asset_type, index: slot_index }; let info = hw.assets().slot_info(slot); let json = serde_json::to_string(&info).unwrap_or_default(); - vm.push(Value::String(json)); - Ok(500) + ret.push_string(json); + Ok(()) } } } diff --git a/crates/prometeu-core/src/virtual_machine/mod.rs b/crates/prometeu-core/src/virtual_machine/mod.rs index 3a7fe6fc..90525df1 100644 --- a/crates/prometeu-core/src/virtual_machine/mod.rs +++ b/crates/prometeu-core/src/virtual_machine/mod.rs @@ -9,14 +9,79 @@ pub use program::Program; pub use prometeu_bytecode::opcode::OpCode; pub use value::Value; pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine}; +pub use prometeu_bytecode::abi::TrapInfo; + +pub type SyscallId = u32; + +#[derive(Debug, PartialEq, Clone)] +pub enum VmFault { + Trap(u32, String), + Panic(String), +} + +pub struct HostReturn<'a> { + stack: &'a mut Vec +} + +impl<'a> HostReturn<'a> { + pub fn new(stack: &'a mut Vec) -> Self { + Self { stack } + } + pub fn push_bool(&mut self, v: bool) { + self.stack.push(Value::Boolean(v)); + } + pub fn push_int(&mut self, v: i64) { + self.stack.push(Value::Int64(v)); + } + pub fn push_bounded(&mut self, v: u32) -> Result<(), VmFault> { + if v > 0xFFFF { + return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_OOB, format!( + "bounded overflow: {}", v + ))); + } + self.stack.push(Value::Bounded(v)); + Ok(()) + } + pub fn push_null(&mut self) { + self.stack.push(Value::Null); + } + pub fn push_gate(&mut self, g: usize) { + self.stack.push(Value::Gate(g)); + } + pub fn push_string(&mut self, s: String) { + self.stack.push(Value::String(s)); + } +} pub trait NativeInterface { /// Dispatches a syscall from the Virtual Machine to the native implementation. /// - /// ABI Rule: Arguments for the syscall are expected on the `operand_stack` in call order. - /// Since the stack is LIFO, the last argument of the call is the first to be popped. + /// ABI Rule: Arguments for the syscall are passed in `args`. /// - /// The implementation MUST pop all its arguments and SHOULD push a return value if the - /// syscall is defined to return one. - fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result; + /// Returns are written via `ret`. + fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, hw: &mut dyn HardwareBridge) -> Result<(), VmFault>; +} + +pub fn expect_bounded(args: &[Value], idx: usize) -> Result { + args.get(idx) + .and_then(|v| match v { + Value::Bounded(b) => Some(*b), + _ => None, + }) + .ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Expected bounded at index {}", idx))) +} + +pub fn expect_int(args: &[Value], idx: usize) -> Result { + args.get(idx) + .and_then(|v| v.as_integer()) + .ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Expected integer at index {}", idx))) +} + +pub fn expect_bool(args: &[Value], idx: usize) -> Result { + args.get(idx) + .and_then(|v| match v { + Value::Boolean(b) => Some(*b), + _ => None, + }) + .ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Expected boolean at index {}", idx))) } diff --git a/crates/prometeu-core/src/virtual_machine/value.rs b/crates/prometeu-core/src/virtual_machine/value.rs index af423345..491d66b4 100644 --- a/crates/prometeu-core/src/virtual_machine/value.rs +++ b/crates/prometeu-core/src/virtual_machine/value.rs @@ -20,6 +20,8 @@ pub enum Value { Boolean(bool), /// UTF-8 string. Strings are immutable and usually come from the Constant Pool. String(String), + /// Bounded 16-bit-ish integer. + Bounded(u32), /// A pointer to an object on the heap. Gate(usize), /// Represents the absence of a value (equivalent to `null` or `undefined`). @@ -40,6 +42,7 @@ impl PartialEq for Value { (Value::Float(a), Value::Int64(b)) => *a == *b as f64, (Value::Boolean(a), Value::Boolean(b)) => a == b, (Value::String(a), Value::String(b)) => a == b, + (Value::Bounded(a), Value::Bounded(b)) => a == b, (Value::Gate(a), Value::Gate(b)) => a == b, (Value::Null, Value::Null) => true, _ => false, @@ -55,6 +58,7 @@ impl PartialOrd for Value { (Value::Int32(a), Value::Int64(b)) => (*a as i64).partial_cmp(b), (Value::Int64(a), Value::Int32(b)) => a.partial_cmp(&(*b as i64)), (Value::Float(a), Value::Float(b)) => a.partial_cmp(b), + (Value::Bounded(a), Value::Bounded(b)) => a.partial_cmp(b), (Value::Int32(a), Value::Float(b)) => (*a as f64).partial_cmp(b), (Value::Float(a), Value::Int32(b)) => a.partial_cmp(&(*b as f64)), (Value::Int64(a), Value::Float(b)) => (*a as f64).partial_cmp(b), @@ -72,6 +76,7 @@ impl Value { Value::Int32(i) => Some(*i as f64), Value::Int64(i) => Some(*i as f64), Value::Float(f) => Some(*f), + Value::Bounded(b) => Some(*b as f64), _ => None, } } @@ -81,6 +86,7 @@ impl Value { Value::Int32(i) => Some(*i as i64), Value::Int64(i) => Some(*i), Value::Float(f) => Some(*f as i64), + Value::Bounded(b) => Some(*b as i64), _ => None, } } @@ -90,6 +96,7 @@ impl Value { Value::Int32(i) => i.to_string(), Value::Int64(i) => i.to_string(), Value::Float(f) => f.to_string(), + Value::Bounded(b) => format!("{}b", b), Value::Boolean(b) => b.to_string(), Value::String(s) => s.clone(), Value::Gate(r) => format!("[Gate {}]", r), diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 026d8a30..8a51808c 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -634,9 +634,45 @@ impl VirtualMachine { self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; } OpCode::Syscall => { + let pc_at_syscall = start_pc as u32; + let id = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))?; - let native_cycles = native.syscall(id, self, hw).map_err(|e| LogicalFrameEndingReason::Panic(format!("syscall 0x{:08X} failed: {}", id, e)))?; - self.cycles += native_cycles; + + let syscall = crate::hardware::syscalls::Syscall::from_u32(id).ok_or_else(|| { + LogicalFrameEndingReason::Trap(TrapInfo { + code: prometeu_bytecode::abi::TRAP_INVALID_SYSCALL, + opcode: OpCode::Syscall as u16, + message: format!("Unknown syscall: 0x{:08X}", id), + pc: pc_at_syscall, + }) + })?; + + let args_count = syscall.args_count(); + + let mut args = Vec::with_capacity(args_count); + for _ in 0..args_count { + let v = self.pop().map_err(|_e| { + LogicalFrameEndingReason::Trap(TrapInfo { + code: prometeu_bytecode::abi::TRAP_STACK_UNDERFLOW, + opcode: OpCode::Syscall as u16, + message: "Syscall argument stack underflow".to_string(), + pc: pc_at_syscall, + }) + })?; + args.push(v); + } + args.reverse(); + + let mut ret = crate::virtual_machine::HostReturn::new(&mut self.operand_stack); + native.syscall(id, &args, &mut ret, hw).map_err(|fault| match fault { + crate::virtual_machine::VmFault::Trap(code, msg) => LogicalFrameEndingReason::Trap(TrapInfo { + code, + opcode: OpCode::Syscall as u16, + message: msg, + pc: pc_at_syscall, + }), + crate::virtual_machine::VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg), + })?; } OpCode::FrameSync => { return Ok(()); @@ -758,12 +794,12 @@ impl VirtualMachine { mod tests { use super::*; use crate::hardware::HardwareBridge; - use crate::virtual_machine::Value; + use crate::virtual_machine::{Value, HostReturn, VmFault, expect_int}; struct MockNative; impl NativeInterface for MockNative { - fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine, _hw: &mut dyn HardwareBridge) -> Result { - Ok(0) + fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { + Ok(()) } } @@ -1314,7 +1350,7 @@ mod tests { let mut hw = crate::Hardware::new(); struct TestNative; impl NativeInterface for TestNative { - fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine, _hw: &mut dyn HardwareBridge) -> Result { Ok(0) } + fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { Ok(()) } } let mut native = TestNative; @@ -1322,4 +1358,156 @@ mod tests { let result = vm.run_budget(100, &mut native, &mut hw).expect("VM run failed"); assert_eq!(result.reason, LogicalFrameEndingReason::EndOfRom); } + + #[test] + fn test_syscall_abi_multi_slot_return() { + let rom = vec![ + 0x70, 0x00, // Syscall + Reserved + 0x01, 0x00, 0x00, 0x00, // Syscall ID 1 + ]; + + struct MultiReturnNative; + impl NativeInterface for MultiReturnNative { + fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { + ret.push_bool(true); + ret.push_int(42); + ret.push_bounded(255)?; + Ok(()) + } + } + + let mut vm = VirtualMachine::new(rom, vec![]); + let mut native = MultiReturnNative; + let mut hw = crate::Hardware::new(); + + vm.prepare_call("0"); + vm.run_budget(100, &mut native, &mut hw).unwrap(); + + assert_eq!(vm.pop().unwrap(), Value::Bounded(255)); + assert_eq!(vm.pop().unwrap(), Value::Int64(42)); + assert_eq!(vm.pop().unwrap(), Value::Boolean(true)); + } + + #[test] + fn test_syscall_abi_void_return() { + let rom = vec![ + 0x70, 0x00, // Syscall + Reserved + 0x01, 0x00, 0x00, 0x00, // Syscall ID 1 + ]; + + struct VoidReturnNative; + impl NativeInterface for VoidReturnNative { + fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { + Ok(()) + } + } + + let mut vm = VirtualMachine::new(rom, vec![]); + let mut native = VoidReturnNative; + let mut hw = crate::Hardware::new(); + + vm.prepare_call("0"); + vm.operand_stack.push(Value::Int32(100)); + vm.run_budget(100, &mut native, &mut hw).unwrap(); + + assert_eq!(vm.pop().unwrap(), Value::Int32(100)); + assert!(vm.operand_stack.is_empty()); + } + + #[test] + fn test_syscall_arg_type_mismatch_trap() { + // GfxClear (0x1001) takes 1 argument + let rom = vec![ + 0x16, 0x00, // PushBool + Reserved + 0x01, // value 1 (true) + 0x70, 0x00, // Syscall + Reserved + 0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001 + ]; + + struct ArgCheckNative; + impl NativeInterface for ArgCheckNative { + fn syscall(&mut self, _id: u32, args: &[Value], _ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> { + expect_int(args, 0)?; + Ok(()) + } + } + + let mut vm = VirtualMachine::new(rom, vec![]); + let mut native = ArgCheckNative; + let mut hw = crate::Hardware::new(); + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_TYPE); + assert_eq!(trap.opcode, OpCode::Syscall as u16); + } + _ => panic!("Expected Trap, got {:?}", report.reason), + } + } + + #[test] + fn test_invalid_syscall_trap() { + let rom = vec![ + 0x70, 0x00, // Syscall + Reserved + 0xEF, 0xBE, 0xAD, 0xDE, // 0xDEADBEEF + ]; + let mut vm = VirtualMachine::new(rom, vec![]); + let mut native = MockNative; + let mut hw = MockHardware; + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_INVALID_SYSCALL); + assert_eq!(trap.opcode, OpCode::Syscall as u16); + assert!(trap.message.contains("Unknown syscall")); + assert_eq!(trap.pc, 0); + } + _ => panic!("Expected Trap, got {:?}", report.reason), + } + } + + #[test] + fn test_syscall_arg_underflow_trap() { + // GfxClear (0x1001) expects 1 arg + let rom = vec![ + 0x70, 0x00, // Syscall + Reserved + 0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001 + ]; + let mut vm = VirtualMachine::new(rom, vec![]); + let mut native = MockNative; + let mut hw = MockHardware; + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_STACK_UNDERFLOW); + assert_eq!(trap.opcode, OpCode::Syscall as u16); + assert!(trap.message.contains("underflow")); + assert_eq!(trap.pc, 0); + } + _ => panic!("Expected Trap, got {:?}", report.reason), + } + } + + #[test] + fn test_host_return_bounded_overflow_trap() { + let mut stack = Vec::new(); + let mut ret = HostReturn::new(&mut stack); + let res = ret.push_bounded(65536); + assert!(res.is_err()); + match res.err().unwrap() { + crate::virtual_machine::VmFault::Trap(code, _) => { + assert_eq!(code, prometeu_bytecode::abi::TRAP_OOB); + } + _ => panic!("Expected Trap"), + } + } } diff --git a/docs/specs/pbs/files/PRs para Junie Global.md b/docs/specs/pbs/files/PRs para Junie Global.md index 66a97bbb..77596171 100644 --- a/docs/specs/pbs/files/PRs para Junie Global.md +++ b/docs/specs/pbs/files/PRs para Junie Global.md @@ -1,6 +1,28 @@ -> **Hard constraints:** +> **Status:** Ready to copy/paste to Junie > -> * `ir_core` and `ir_vm` remain **fully decoupled**. -> * The only contact point is lowering (`core_to_vm`). -> * **No placeholders**, no guessed offsets, no runtime inference of language semantics. -> * Every PR must include tests. \ No newline at end of file +> **Goal:** expose hardware types **1:1** to PBS and VM as **SAFE builtins** (stack/value), *not* HIP. +> +> **Key constraint:** Prometeu does **not** have `u16` as a primitive. Use `bounded` for 16-bit-ish hardware scalars. +> +> **Deliverables (in order):** +> +> 1. VM hostcall ABI supports returning **flattened SAFE structs** (multi-slot). +> 2. PBS prelude defines `Color`, `ButtonState`, `Pad`, `Touch` using `bounded`. +> 3. Lowering emits deterministic syscalls for `Gfx.clear(Color)` and `Input.pad()/touch()`. +> 4. Runtime implements the syscalls and an integration cartridge validates behavior. +> +> **Hard rules (do not break):** +> +> * No heap, no gates, no HIP for these types. +> * No `u16` anywhere in PBS surface. +> * Returned structs are *values*, copied by stack. +> * Every PR must include tests. +> * No renumbering opcodes; append only. + +## Notes / Forbidden + +* DO NOT introduce `u16` into PBS. +* DO NOT allocate heap for these types. +* DO NOT encode `Pad`/`Touch` as gates. +* DO NOT change unrelated opcodes. +* DO NOT add “convenient” APIs not listed above. diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index e86752ee..d0a1d604 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -1,47 +1,234 @@ -## PR-11 — Cross-Layer Conformance Tests: Core→VM→Bytecode (HIP) +## PR-02 — PBS Prelude: Add SAFE builtins for Color / ButtonState / Pad / Touch (bounded) ### Goal -Prove end-to-end determinism and stability. +Expose hardware types to PBS scripts as **value structs** using `bounded` (no `u16`). -### Required Tests +### Required PBS definitions (in prelude / hardware module) -1. PBS snippet (or Core IR fixture) that: +> Put these in the standard library surface that PBS sees without user creating them. -* allocates a storage struct -* mutates a field -* peeks value +```pbs +pub declare struct Color(value: bounded) +[ + (r: int, g: int, b: int): (0b) as rgb + { + ... + } +] +[[ + BLACK: (...) {} + WHITE: (...) {} + RED: (...) {} + GREEN: (...) {} + BLUE: (...) {} + MAGENTA: (...) {} + TRANSPARENT: (...) {} + COLOR_KEY: (...) {} +]] +{ + pub fn raw(self: Color): bounded; +} -Assert: +pub declare struct ButtonState( + pressed: bool, + released: bool, + down: bool, + hold_frames: bounded +) -* VM IR contains: +pub declare struct Pad( + up: ButtonState, + down: ButtonState, + left: ButtonState, + right: ButtonState, + a: ButtonState, + b: ButtonState, + x: ButtonState, + y: ButtonState, + l: ButtonState, + r: ButtonState, + start: ButtonState, + select: ButtonState +) +{ + pub fn any(self: Pad): bool; +} - * `Alloc(type_id, slots)` - * `GateBeginMutate/EndMutate` - * `GateStore(offset)` - * `GateBeginPeek/EndPeek` - * `GateLoad(offset)` - * RC ops (retain/release) +pub declare struct Touch( + f: ButtonState, + x: int, + y: int +) +``` -2. Bytecode golden output for the same program: +### Semantics / constraints -* assert the exact bytes match the frozen ISA/ABI. +* `Color.value` stores the hardware RGB565 *raw* as `bounded`. +* `hold_frames` is `bounded`. +* `x/y` remain `int`. + +### Implementation notes (binding) + +* `Color.rgb(r,g,b)` must clamp inputs to 0..255 and then pack to RGB565. +* `Color.raw()` returns the internal bounded. +* `Pad.any()` must be a **pure SAFE** function compiled normally (no hostcall). + +### Tests (mandatory) + +* FE/typecheck: `Color.WHITE` is a `Color`. +* FE/typecheck: `Gfx.clear(Color.WHITE)` typechecks. +* FE/typecheck: `let p: Pad = Input.pad(); if p.any() { }` typechecks. ### Non-goals -* No runtime execution +* No heap types +* No gates --- -## STOP POINT (Hard Gate) +## PR-03 — Lowering: Host Contracts for Gfx/Input using deterministic syscalls -* HIP access is fully deterministic -* RC events are explicit and testable -* HIP ISA/ABI v0 is frozen with golden bytecode tests +### Goal -Only after this point may we implement/tune: +Map PBS host contracts to stable syscalls with a deterministic ABI. + +### Required host contracts in PBS surface + +```pbs +pub declare contract Gfx host +{ + fn clear(color: Color): void; +} + +pub declare contract Input host +{ + fn pad(): Pad; + fn touch(): Touch; +} +``` + +### Required lowering rules + +1. `Gfx.clear(color)` + +* Emit `SYSCALL_GFX_CLEAR` +* ABI: args = [Color.raw] as `bounded` +* returns: void + +2. `Input.pad()` + +* Emit `SYSCALL_INPUT_PAD` +* args: none +* returns: flattened `Pad` in field order as declared + +3. `Input.touch()` + +* Emit `SYSCALL_INPUT_TOUCH` +* args: none +* returns: flattened `Touch` in field order as declared + +### Flattening order (binding) + +**ButtonState** returns 4 slots in order: + +1. pressed (bool) +2. released (bool) +3. down (bool) +4. hold_frames (bounded) + +**Pad** returns 12 ButtonState blocks in this exact order: +`up, down, left, right, a, b, x, y, l, r, start, select` + +**Touch** returns: + +1. f (ButtonState block) +2. x (int) +3. y (int) + +### Tests (mandatory) + +* Lowering golden test: `Gfx.clear(Color.WHITE)` emits `SYSCALL_GFX_CLEAR` with 1 arg. +* Lowering golden test: `Input.pad()` emits `SYSCALL_INPUT_PAD` and assigns to local. +* Lowering golden test: `Input.touch()` emits `SYSCALL_INPUT_TOUCH`. + +### Non-goals + +* No runtime changes +* No VM heap + +--- + +## PR-04 — Runtime: Implement syscalls for Color/Gfx and Input pad/touch + integration cartridge + +### Goal + +Make the new syscalls actually work and prove them with an integration test cartridge. + +### Required syscall implementations + +#### 1) `SYSCALL_GFX_CLEAR` + +* Read 1 arg: `bounded` raw color +* Convert to `u16` internally (runtime-only) + + * If raw > 0xFFFF, trap `TRAP_OOB` or `TRAP_TYPE` (choose one and document) +* Fill framebuffer with that RGB565 value + +#### 2) `SYSCALL_INPUT_PAD` + +* No args +* Snapshot the current runtime `Pad` and push a flattened `Pad` return: + + * For each button: pressed, released, down, hold_frames + * hold_frames pushed as `bounded` + +#### 3) `SYSCALL_INPUT_TOUCH` + +* No args +* Snapshot `Touch` and push flattened `Touch` return: + + * f ButtonState + * x int + * y int + +### Integration cartridge (mandatory) + +Add `test-cartridges/hw_hello` (or similar) with: + +```pbs +fn frame(): void +{ + // 1) clear screen white + Gfx.clear(Color.WHITE); + + // 2) read pad and branch + let p: Pad = Input.pad(); + if p.any() { + Gfx.clear(Color.MAGENTA); + } + + // 3) read touch and branch on f.down + let t: Touch = Input.touch(); + if t.f.down { + // choose a third color to prove the struct returned correctly + Gfx.clear(Color.BLUE); + } +} +``` + +### Acceptance criteria + +* Cartridge runs without VM faults. +* With no input: screen is WHITE. +* With any pad button held: screen becomes MAGENTA. +* With touch f.down: screen becomes BLUE. + +### Tests (mandatory) + +* Runtime unit test: `SYSCALL_GFX_CLEAR` rejects raw > 0xFFFF deterministically. +* Runtime unit test: `SYSCALL_INPUT_PAD` returns correct number of stack slots (48). +* Runtime unit test: `SYSCALL_INPUT_TOUCH` returns correct number of stack slots (4 + 2 = 6). + +--- -* Gate Pool -* Heap allocation -* RC counters + safe point reclaim -* Traps at runtime