This commit is contained in:
Nilton Constantino 2026-01-31 00:22:34 +00:00
parent 8a89b480c4
commit 373f1190e2
No known key found for this signature in database
8 changed files with 820 additions and 269 deletions

View File

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

View File

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

View File

@ -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<u64, String> {
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<u64, String> {
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<String> = 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(())
}
}
}

View File

@ -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<Value>
}
impl<'a> HostReturn<'a> {
pub fn new(stack: &'a mut Vec<Value>) -> 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<u64, String>;
/// 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<u32, VmFault> {
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<i64, VmFault> {
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<bool, VmFault> {
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)))
}

View File

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

View File

@ -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<u64, String> {
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<u64, String> { 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"),
}
}
}

View File

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

View File

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