pr 37
This commit is contained in:
parent
8a89b480c4
commit
373f1190e2
@ -36,6 +36,10 @@ pub const TRAP_DEAD_GATE: u32 = 0x02;
|
|||||||
pub const TRAP_OOB: u32 = 0x03;
|
pub const TRAP_OOB: u32 = 0x03;
|
||||||
/// Attempted a typed operation on a gate whose storage type does not match.
|
/// Attempted a typed operation on a gate whose storage type does not match.
|
||||||
pub const TRAP_TYPE: u32 = 0x04;
|
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.
|
/// Detailed information about a runtime trap.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -74,6 +78,8 @@ mod tests {
|
|||||||
assert_eq!(TRAP_DEAD_GATE, 0x02);
|
assert_eq!(TRAP_DEAD_GATE, 0x02);
|
||||||
assert_eq!(TRAP_OOB, 0x03);
|
assert_eq!(TRAP_OOB, 0x03);
|
||||||
assert_eq!(TRAP_TYPE, 0x04);
|
assert_eq!(TRAP_TYPE, 0x04);
|
||||||
|
assert_eq!(TRAP_INVALID_SYSCALL, 0x07);
|
||||||
|
assert_eq!(TRAP_STACK_UNDERFLOW, 0x08);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -86,6 +92,10 @@ HIP Traps:
|
|||||||
- OOB (0x03): Access beyond allocated slots.
|
- OOB (0x03): Access beyond allocated slots.
|
||||||
- TYPE (0x04): Type mismatch during heap access.
|
- TYPE (0x04): Type mismatch during heap access.
|
||||||
|
|
||||||
|
System Traps:
|
||||||
|
- INVALID_SYSCALL (0x07): Unknown syscall ID.
|
||||||
|
- STACK_UNDERFLOW (0x08): Missing syscall arguments.
|
||||||
|
|
||||||
Operand Sizes:
|
Operand Sizes:
|
||||||
- Alloc: 8 bytes (u32 type_id, u32 slots)
|
- Alloc: 8 bytes (u32 type_id, u32 slots)
|
||||||
- GateLoad: 4 bytes (u32 offset)
|
- GateLoad: 4 bytes (u32 offset)
|
||||||
@ -95,8 +105,9 @@ Operand Sizes:
|
|||||||
// This test serves as a "doc-lock".
|
// This test serves as a "doc-lock".
|
||||||
// If you change the ABI, you must update this string.
|
// If you change the ABI, you must update this string.
|
||||||
let current_info = format!(
|
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_GATE, TRAP_DEAD_GATE, TRAP_OOB, TRAP_TYPE,
|
||||||
|
TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW,
|
||||||
operand_size(OpCode::Alloc),
|
operand_size(OpCode::Alloc),
|
||||||
operand_size(OpCode::GateLoad),
|
operand_size(OpCode::GateLoad),
|
||||||
operand_size(OpCode::GateStore),
|
operand_size(OpCode::GateStore),
|
||||||
|
|||||||
@ -149,4 +149,46 @@ impl Syscall {
|
|||||||
_ => None,
|
_ => 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use crate::log::{LogLevel, LogService, LogSource};
|
|||||||
use crate::model::{BankType, Cartridge, Color};
|
use crate::model::{BankType, Cartridge, Color};
|
||||||
use crate::prometeu_os::NativeInterface;
|
use crate::prometeu_os::NativeInterface;
|
||||||
use crate::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
|
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::collections::HashMap;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@ -324,14 +324,14 @@ impl PrometeuOS {
|
|||||||
|
|
||||||
|
|
||||||
// Helper para syscalls
|
// 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 {
|
let level = match level_val {
|
||||||
0 => LogLevel::Trace,
|
0 => LogLevel::Trace,
|
||||||
1 => LogLevel::Debug,
|
1 => LogLevel::Debug,
|
||||||
2 => LogLevel::Info,
|
2 => LogLevel::Info,
|
||||||
3 => LogLevel::Warn,
|
3 => LogLevel::Warn,
|
||||||
4 => LogLevel::Error,
|
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;
|
let app_id = self.current_app_id;
|
||||||
@ -342,8 +342,7 @@ impl PrometeuOS {
|
|||||||
self.logs_written_this_frame.insert(app_id, count + 1);
|
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());
|
self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string());
|
||||||
}
|
}
|
||||||
vm.push(Value::Null);
|
return Ok(());
|
||||||
return Ok(50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logs_written_this_frame.insert(app_id, count + 1);
|
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);
|
self.log(level, LogSource::App { app_id }, tag, final_msg);
|
||||||
|
|
||||||
vm.push(Value::Null);
|
Ok(())
|
||||||
Ok(100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_color(&self, value: i64) -> Color {
|
pub fn get_color(&self, value: i64) -> Color {
|
||||||
@ -411,6 +409,17 @@ mod tests {
|
|||||||
use crate::virtual_machine::{Value, VirtualMachine};
|
use crate::virtual_machine::{Value, VirtualMachine};
|
||||||
use crate::Hardware;
|
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]
|
#[test]
|
||||||
fn test_infinite_loop_budget_reset_bug() {
|
fn test_infinite_loop_budget_reset_bug() {
|
||||||
let mut os = PrometeuOS::new(None);
|
let mut os = PrometeuOS::new(None);
|
||||||
@ -542,7 +551,7 @@ mod tests {
|
|||||||
vm.push(Value::Boolean(false)); // arg9: flipY
|
vm.push(Value::Boolean(false)); // arg9: flipY
|
||||||
vm.push(Value::Int32(4)); // arg10: priority
|
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());
|
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::Int32(0));
|
||||||
vm.push(Value::String("mouse_cursor".to_string())); // arg1?
|
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!(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]
|
#[test]
|
||||||
@ -580,7 +593,7 @@ mod tests {
|
|||||||
// 1. Normal log test
|
// 1. Normal log test
|
||||||
vm.push(Value::Int64(2)); // Info
|
vm.push(Value::Int64(2)); // Info
|
||||||
vm.push(Value::String("Hello Log".to_string()));
|
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());
|
assert!(res.is_ok());
|
||||||
|
|
||||||
let recent = os.log_service.get_recent(1);
|
let recent = os.log_service.get_recent(1);
|
||||||
@ -592,7 +605,7 @@ mod tests {
|
|||||||
let long_msg = "A".repeat(300);
|
let long_msg = "A".repeat(300);
|
||||||
vm.push(Value::Int64(3)); // Warn
|
vm.push(Value::Int64(3)); // Warn
|
||||||
vm.push(Value::String(long_msg));
|
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);
|
let recent = os.log_service.get_recent(1);
|
||||||
assert_eq!(recent[0].msg.len(), 256);
|
assert_eq!(recent[0].msg.len(), 256);
|
||||||
@ -603,13 +616,13 @@ mod tests {
|
|||||||
for i in 0..8 {
|
for i in 0..8 {
|
||||||
vm.push(Value::Int64(2));
|
vm.push(Value::Int64(2));
|
||||||
vm.push(Value::String(format!("Log {}", i)));
|
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)
|
// The 11th log should be ignored (and generate a system warning)
|
||||||
vm.push(Value::Int64(2));
|
vm.push(Value::Int64(2));
|
||||||
vm.push(Value::String("Eleventh log".to_string()));
|
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);
|
let recent = os.log_service.get_recent(2);
|
||||||
// The last log should be the rate limit warning (came after the 10th log attempted)
|
// 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);
|
os.begin_logical_frame(&InputSignals::default(), &mut hw);
|
||||||
vm.push(Value::Int64(2));
|
vm.push(Value::Int64(2));
|
||||||
vm.push(Value::String("New frame log".to_string()));
|
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);
|
let recent = os.log_service.get_recent(1);
|
||||||
assert_eq!(recent[0].msg, "New frame log");
|
assert_eq!(recent[0].msg, "New frame log");
|
||||||
@ -631,7 +644,7 @@ mod tests {
|
|||||||
vm.push(Value::Int64(2)); // Info
|
vm.push(Value::Int64(2)); // Info
|
||||||
vm.push(Value::Int64(42)); // Tag
|
vm.push(Value::Int64(42)); // Tag
|
||||||
vm.push(Value::String("Tagged Log".to_string()));
|
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);
|
let recent = os.log_service.get_recent(1);
|
||||||
assert_eq!(recent[0].msg, "Tagged Log");
|
assert_eq!(recent[0].msg, "Tagged Log");
|
||||||
@ -640,7 +653,7 @@ mod tests {
|
|||||||
|
|
||||||
// 6. GFX Syscall return test
|
// 6. GFX Syscall return test
|
||||||
vm.push(Value::Int64(1)); // color_idx
|
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);
|
assert_eq!(vm.pop().unwrap(), Value::Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -682,6 +695,21 @@ mod tests {
|
|||||||
assert!(!os.logical_frame_active);
|
assert!(!os.logical_frame_active);
|
||||||
assert!(vm.call_stack.is_empty());
|
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 {
|
impl NativeInterface for PrometeuOS {
|
||||||
@ -696,113 +724,112 @@ impl NativeInterface for PrometeuOS {
|
|||||||
/// - 0x5000: Logging
|
/// - 0x5000: Logging
|
||||||
///
|
///
|
||||||
/// Each syscall returns the number of virtual cycles it consumed.
|
/// 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;
|
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 {
|
match syscall {
|
||||||
// --- System Syscalls ---
|
// --- System Syscalls ---
|
||||||
|
|
||||||
// system.has_cart() -> bool
|
// system.has_cart() -> bool
|
||||||
Syscall::SystemHasCart => {
|
Syscall::SystemHasCart => {
|
||||||
// Returns true if a cartridge is available.
|
ret.push_bool(true);
|
||||||
vm.push(Value::Boolean(true)); // For now, assume true or check state
|
Ok(())
|
||||||
Ok(10)
|
|
||||||
}
|
}
|
||||||
// system.run_cart() -> null
|
// system.run_cart() -> null
|
||||||
Syscall::SystemRunCart => {
|
Syscall::SystemRunCart => {
|
||||||
// Triggers loading and execution of the current cartridge.
|
ret.push_null();
|
||||||
vm.push(Value::Null);
|
Ok(())
|
||||||
Ok(100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GFX Syscalls ---
|
// --- GFX Syscalls ---
|
||||||
|
|
||||||
// gfx.clear(color_index) -> null
|
// gfx.clear(color_index) -> null
|
||||||
Syscall::GfxClear => {
|
Syscall::GfxClear => {
|
||||||
let color_val = vm.pop_integer()?;
|
let color_val = expect_int(args, 0)?;
|
||||||
let color = self.get_color(color_val);
|
let color = self.get_color(color_val);
|
||||||
hw.gfx_mut().clear(color);
|
hw.gfx_mut().clear(color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
// gfx.draw_rect(x, y, w, h, color_index) -> null
|
// gfx.draw_rect(x, y, w, h, color_index) -> null
|
||||||
Syscall::GfxFillRect => {
|
Syscall::GfxFillRect => {
|
||||||
let color_val = vm.pop_integer()?;
|
let x = expect_int(args, 0)? as i32;
|
||||||
let h = vm.pop_integer()? as i32;
|
let y = expect_int(args, 1)? as i32;
|
||||||
let w = vm.pop_integer()? as i32;
|
let w = expect_int(args, 2)? as i32;
|
||||||
let y = vm.pop_integer()? as i32;
|
let h = expect_int(args, 3)? as i32;
|
||||||
let x = vm.pop_integer()? as i32;
|
let color_val = expect_int(args, 4)?;
|
||||||
let color = self.get_color(color_val);
|
let color = self.get_color(color_val);
|
||||||
hw.gfx_mut().fill_rect(x, y, w, h, color);
|
hw.gfx_mut().fill_rect(x, y, w, h, color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(200)
|
Ok(())
|
||||||
}
|
}
|
||||||
// gfx.draw_line(x1, y1, x2, y2, color_index) -> null
|
// gfx.draw_line(x1, y1, x2, y2, color_index) -> null
|
||||||
Syscall::GfxDrawLine => {
|
Syscall::GfxDrawLine => {
|
||||||
let color_val = vm.pop_integer()?;
|
let x1 = expect_int(args, 0)? as i32;
|
||||||
let y2 = vm.pop_integer()? as i32;
|
let y1 = expect_int(args, 1)? as i32;
|
||||||
let x2 = vm.pop_integer()? as i32;
|
let x2 = expect_int(args, 2)? as i32;
|
||||||
let y1 = vm.pop_integer()? as i32;
|
let y2 = expect_int(args, 3)? as i32;
|
||||||
let x1 = vm.pop_integer()? as i32;
|
let color_val = expect_int(args, 4)?;
|
||||||
let color = self.get_color(color_val);
|
let color = self.get_color(color_val);
|
||||||
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
|
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(200)
|
Ok(())
|
||||||
}
|
}
|
||||||
// gfx.draw_circle(x, y, r, color_index) -> null
|
// gfx.draw_circle(x, y, r, color_index) -> null
|
||||||
Syscall::GfxDrawCircle => {
|
Syscall::GfxDrawCircle => {
|
||||||
let color_val = vm.pop_integer()?;
|
let x = expect_int(args, 0)? as i32;
|
||||||
let r = vm.pop_integer()? as i32;
|
let y = expect_int(args, 1)? as i32;
|
||||||
let y = vm.pop_integer()? as i32;
|
let r = expect_int(args, 2)? as i32;
|
||||||
let x = vm.pop_integer()? as i32;
|
let color_val = expect_int(args, 3)?;
|
||||||
let color = self.get_color(color_val);
|
let color = self.get_color(color_val);
|
||||||
hw.gfx_mut().draw_circle(x, y, r, color);
|
hw.gfx_mut().draw_circle(x, y, r, color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(200)
|
Ok(())
|
||||||
}
|
}
|
||||||
// gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) -> null
|
// gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) -> null
|
||||||
Syscall::GfxDrawDisc => {
|
Syscall::GfxDrawDisc => {
|
||||||
let fill_color_val = vm.pop_integer()?;
|
let x = expect_int(args, 0)? as i32;
|
||||||
let border_color_val = vm.pop_integer()?;
|
let y = expect_int(args, 1)? as i32;
|
||||||
let r = vm.pop_integer()? as i32;
|
let r = expect_int(args, 2)? as i32;
|
||||||
let y = vm.pop_integer()? as i32;
|
let border_color_val = expect_int(args, 3)?;
|
||||||
let x = vm.pop_integer()? as i32;
|
let fill_color_val = expect_int(args, 4)?;
|
||||||
let fill_color = self.get_color(fill_color_val);
|
let fill_color = self.get_color(fill_color_val);
|
||||||
let border_color = self.get_color(border_color_val);
|
let border_color = self.get_color(border_color_val);
|
||||||
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
|
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(300)
|
Ok(())
|
||||||
}
|
}
|
||||||
// gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) -> null
|
// gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) -> null
|
||||||
Syscall::GfxDrawSquare => {
|
Syscall::GfxDrawSquare => {
|
||||||
let fill_color_val = vm.pop_integer()?;
|
let x = expect_int(args, 0)? as i32;
|
||||||
let border_color_val = vm.pop_integer()?;
|
let y = expect_int(args, 1)? as i32;
|
||||||
let h = vm.pop_integer()? as i32;
|
let w = expect_int(args, 2)? as i32;
|
||||||
let w = vm.pop_integer()? as i32;
|
let h = expect_int(args, 3)? as i32;
|
||||||
let y = vm.pop_integer()? as i32;
|
let border_color_val = expect_int(args, 4)?;
|
||||||
let x = vm.pop_integer()? as i32;
|
let fill_color_val = expect_int(args, 5)?;
|
||||||
let fill_color = self.get_color(fill_color_val);
|
let fill_color = self.get_color(fill_color_val);
|
||||||
let border_color = self.get_color(border_color_val);
|
let border_color = self.get_color(border_color_val);
|
||||||
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
|
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(200)
|
Ok(())
|
||||||
}
|
}
|
||||||
// gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority)
|
// gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority)
|
||||||
Syscall::GfxSetSprite => {
|
Syscall::GfxSetSprite => {
|
||||||
let priority = vm.pop_integer()? as u8;
|
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
|
||||||
let flip_y = vm.pop_integer()? != 0;
|
Value::String(s) => s.clone(),
|
||||||
let flip_x = vm.pop_integer()? != 0;
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())),
|
||||||
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 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);
|
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,
|
priority,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::GfxDrawText => {
|
Syscall::GfxDrawText => {
|
||||||
let color_val = vm.pop_integer()?;
|
let x = expect_int(args, 0)? as i32;
|
||||||
let color = self.get_color(color_val);
|
let y = expect_int(args, 1)? as i32;
|
||||||
let msg = match vm.pop()? {
|
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s.clone(),
|
||||||
_ => return Err("Expected string message".into()),
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
|
||||||
};
|
};
|
||||||
let y = vm.pop_integer()? as i32;
|
let color_val = expect_int(args, 3)?;
|
||||||
let x = vm.pop_integer()? as i32;
|
let color = self.get_color(color_val);
|
||||||
hw.gfx_mut().draw_text(x, y, &msg, color);
|
hw.gfx_mut().draw_text(x, y, &msg, color);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Input Syscalls ---
|
// --- Input Syscalls ---
|
||||||
|
|
||||||
// input.get_pad(button_id) -> bool
|
// input.get_pad(button_id) -> bool
|
||||||
Syscall::InputGetPad => {
|
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);
|
let is_down = self.is_button_down(button_id, hw);
|
||||||
vm.push(Value::Boolean(is_down));
|
ret.push_bool(is_down);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::InputGetPadPressed => {
|
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);
|
let val = self.get_button(button_id, hw).map(|b| b.pressed).unwrap_or(false);
|
||||||
vm.push(Value::Boolean(val));
|
ret.push_bool(val);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::InputGetPadReleased => {
|
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);
|
let val = self.get_button(button_id, hw).map(|b| b.released).unwrap_or(false);
|
||||||
vm.push(Value::Boolean(val));
|
ret.push_bool(val);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::InputGetPadHold => {
|
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);
|
let val = self.get_button(button_id, hw).map(|b| b.hold_frames).unwrap_or(0);
|
||||||
vm.push(Value::Int32(val as i32));
|
ret.push_int(val as i64);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Syscall::TouchGetX => {
|
Syscall::TouchGetX => {
|
||||||
vm.push(Value::Int32(hw.touch().x));
|
ret.push_int(hw.touch().x as i64);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::TouchGetY => {
|
Syscall::TouchGetY => {
|
||||||
vm.push(Value::Int32(hw.touch().y));
|
ret.push_int(hw.touch().y as i64);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::TouchIsDown => {
|
Syscall::TouchIsDown => {
|
||||||
vm.push(Value::Boolean(hw.touch().f.down));
|
ret.push_bool(hw.touch().f.down);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::TouchIsPressed => {
|
Syscall::TouchIsPressed => {
|
||||||
vm.push(Value::Boolean(hw.touch().f.pressed));
|
ret.push_bool(hw.touch().f.pressed);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::TouchIsReleased => {
|
Syscall::TouchIsReleased => {
|
||||||
vm.push(Value::Boolean(hw.touch().f.released));
|
ret.push_bool(hw.touch().f.released);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::TouchGetHold => {
|
Syscall::TouchGetHold => {
|
||||||
vm.push(Value::Int32(hw.touch().f.hold_frames as i32));
|
ret.push_int(hw.touch().f.hold_frames as i64);
|
||||||
Ok(50)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Audio Syscalls ---
|
// --- Audio Syscalls ---
|
||||||
|
|
||||||
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
|
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
|
||||||
Syscall::AudioPlaySample => {
|
Syscall::AudioPlaySample => {
|
||||||
let pitch = vm.pop_number()?;
|
let sample_id = expect_int(args, 0)? as u32;
|
||||||
let pan = vm.pop_integer()? as u8;
|
let voice_id = expect_int(args, 1)? as usize;
|
||||||
let volume = vm.pop_integer()? as u8;
|
let volume = expect_int(args, 2)? as u8;
|
||||||
let voice_id = vm.pop_integer()? as usize;
|
let pan = expect_int(args, 3)? as u8;
|
||||||
let sample_id = vm.pop_integer()? as u32;
|
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);
|
hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, crate::hardware::LoopMode::Off);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(300)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode)
|
// audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode)
|
||||||
Syscall::AudioPlay => {
|
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,
|
0 => crate::hardware::LoopMode::Off,
|
||||||
_ => crate::hardware::LoopMode::On,
|
_ => 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);
|
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);
|
hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(300)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Filesystem Syscalls (0x4000) ---
|
// --- Filesystem Syscalls (0x4000) ---
|
||||||
|
|
||||||
// FS_OPEN(path) -> handle
|
// FS_OPEN(path) -> handle
|
||||||
// Opens a file in the virtual sandbox and returns a numeric handle.
|
|
||||||
Syscall::FsOpen => {
|
Syscall::FsOpen => {
|
||||||
let path = match vm.pop()? {
|
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s.clone(),
|
||||||
_ => return Err("Expected string path".into()),
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
|
||||||
};
|
};
|
||||||
if self.fs_state != FsState::Mounted {
|
if self.fs_state != FsState::Mounted {
|
||||||
vm.push(Value::Int64(-1));
|
ret.push_int(-1);
|
||||||
return Ok(100);
|
return Ok(());
|
||||||
}
|
}
|
||||||
let handle = self.next_handle;
|
let handle = self.next_handle;
|
||||||
self.open_files.insert(handle, path);
|
self.open_files.insert(handle, path);
|
||||||
self.next_handle += 1;
|
self.next_handle += 1;
|
||||||
vm.push(Value::Int64(handle as i64));
|
ret.push_int(handle as i64);
|
||||||
Ok(200)
|
Ok(())
|
||||||
}
|
}
|
||||||
// FS_READ(handle) -> content
|
// FS_READ(handle) -> content
|
||||||
Syscall::FsRead => {
|
Syscall::FsRead => {
|
||||||
let handle = vm.pop_integer()? as u32;
|
let handle = expect_int(args, 0)? as u32;
|
||||||
let path = self.open_files.get(&handle).ok_or("Invalid handle")?;
|
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
|
||||||
match self.fs.read_file(path) {
|
match self.fs.read_file(path) {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let s = String::from_utf8_lossy(&data).into_owned();
|
let s = String::from_utf8_lossy(&data).into_owned();
|
||||||
vm.push(Value::String(s));
|
ret.push_string(s);
|
||||||
Ok(1000)
|
|
||||||
}
|
|
||||||
Err(_e) => {
|
|
||||||
vm.push(Value::Null);
|
|
||||||
Ok(100)
|
|
||||||
}
|
}
|
||||||
|
Err(_) => ret.push_null(),
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
// FS_WRITE(handle, content)
|
// FS_WRITE(handle, content)
|
||||||
Syscall::FsWrite => {
|
Syscall::FsWrite => {
|
||||||
let content = match vm.pop()? {
|
let handle = expect_int(args, 0)? as u32;
|
||||||
Value::String(s) => s,
|
let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? {
|
||||||
_ => return Err("Expected string 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_else(|| VmFault::Panic("Invalid handle".into()))?;
|
||||||
let path = self.open_files.get(&handle).ok_or("Invalid handle")?;
|
match self.fs.write_file(path, &content) {
|
||||||
match self.fs.write_file(path, content.as_bytes()) {
|
Ok(_) => ret.push_bool(true),
|
||||||
Ok(_) => {
|
Err(_) => ret.push_bool(false),
|
||||||
vm.push(Value::Boolean(true));
|
|
||||||
Ok(1000)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
vm.push(Value::Boolean(false));
|
|
||||||
Ok(100)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
// FS_CLOSE(handle)
|
// FS_CLOSE(handle)
|
||||||
Syscall::FsClose => {
|
Syscall::FsClose => {
|
||||||
let handle = vm.pop_integer()? as u32;
|
let handle = expect_int(args, 0)? as u32;
|
||||||
self.open_files.remove(&handle);
|
self.open_files.remove(&handle);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
// FS_LISTDIR(path)
|
// FS_LIST_DIR(path)
|
||||||
Syscall::FsListDir => {
|
Syscall::FsListDir => {
|
||||||
let path = match vm.pop()? {
|
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s.clone(),
|
||||||
_ => return Err("Expected string path".into()),
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
|
||||||
};
|
};
|
||||||
match self.fs.list_dir(&path) {
|
match self.fs.list_dir(&path) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
// Returns a string separated by ';' for simple parsing in PVM.
|
|
||||||
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
|
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
|
||||||
vm.push(Value::String(names.join(";")));
|
ret.push_string(names.join(";"));
|
||||||
Ok(500)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
vm.push(Value::Null);
|
|
||||||
Ok(100)
|
|
||||||
}
|
}
|
||||||
|
Err(_) => ret.push_null(),
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
// FS_EXISTS(path) -> bool
|
// FS_EXISTS(path)
|
||||||
Syscall::FsExists => {
|
Syscall::FsExists => {
|
||||||
let path = match vm.pop()? {
|
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s.clone(),
|
||||||
_ => return Err("Expected string path".into()),
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
|
||||||
};
|
};
|
||||||
vm.push(Value::Boolean(self.fs.exists(&path)));
|
ret.push_bool(self.fs.exists(&path));
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
// FS_DELETE(path)
|
// FS_DELETE(path)
|
||||||
Syscall::FsDelete => {
|
Syscall::FsDelete => {
|
||||||
let path = match vm.pop()? {
|
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s.clone(),
|
||||||
_ => return Err("Expected string path".into()),
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
|
||||||
};
|
};
|
||||||
match self.fs.delete(&path) {
|
match self.fs.delete(&path) {
|
||||||
Ok(_) => vm.push(Value::Boolean(true)),
|
Ok(_) => ret.push_bool(true),
|
||||||
Err(_) => vm.push(Value::Boolean(false)),
|
Err(_) => ret.push_bool(false),
|
||||||
}
|
}
|
||||||
Ok(500)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Log Syscalls (0x5000) ---
|
// --- Log Syscalls (0x5000) ---
|
||||||
|
|
||||||
// LOG_WRITE(level, msg)
|
// LOG_WRITE(level, msg)
|
||||||
Syscall::LogWrite => {
|
Syscall::LogWrite => {
|
||||||
let msg = match vm.pop()? {
|
let level = expect_int(args, 0)?;
|
||||||
Value::String(s) => s,
|
let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
|
||||||
_ => return Err("Expected string 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(level, 0, msg)?;
|
||||||
self.syscall_log_write(vm, level, 0, msg)
|
ret.push_null();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
// LOG_WRITE_TAG(level, tag, msg)
|
// LOG_WRITE_TAG(level, tag, msg)
|
||||||
Syscall::LogWriteTag => {
|
Syscall::LogWriteTag => {
|
||||||
let msg = match vm.pop()? {
|
let level = expect_int(args, 0)?;
|
||||||
Value::String(s) => s,
|
let tag = expect_int(args, 1)? as u16;
|
||||||
_ => return Err("Expected string message".into()),
|
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;
|
self.syscall_log_write(level, tag, msg)?;
|
||||||
let level = vm.pop_integer()?;
|
ret.push_null();
|
||||||
self.syscall_log_write(vm, level, tag, msg)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Asset Syscalls ---
|
// --- Asset Syscalls ---
|
||||||
Syscall::AssetLoad => {
|
Syscall::AssetLoad => {
|
||||||
let asset_id = match vm.pop()? {
|
let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s.clone(),
|
||||||
_ => return Err("Expected string asset_id".into()),
|
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_id".into())),
|
||||||
};
|
};
|
||||||
let asset_type_val = vm.pop_integer()? as u32;
|
let asset_type_val = expect_int(args, 1)? as u32;
|
||||||
let slot_index = vm.pop_integer()? as usize;
|
let slot_index = expect_int(args, 2)? as usize;
|
||||||
|
|
||||||
let asset_type = match asset_type_val {
|
let asset_type = match asset_type_val {
|
||||||
0 => crate::model::BankType::TILES,
|
0 => crate::model::BankType::TILES,
|
||||||
1 => crate::model::BankType::SOUNDS,
|
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 slot = crate::model::SlotRef { asset_type, index: slot_index };
|
||||||
|
|
||||||
match hw.assets().load(&asset_id, slot) {
|
match hw.assets().load(&asset_id, slot) {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
vm.push(Value::Int64(handle as i64));
|
ret.push_int(handle as i64);
|
||||||
Ok(1000)
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(VmFault::Panic(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Syscall::AssetStatus => {
|
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 = hw.assets().status(handle);
|
||||||
let status_val = match status {
|
let status_val = match status {
|
||||||
crate::model::LoadStatus::PENDING => 0,
|
crate::model::LoadStatus::PENDING => 0,
|
||||||
@ -1086,46 +1115,46 @@ impl NativeInterface for PrometeuOS {
|
|||||||
crate::model::LoadStatus::CANCELED => 4,
|
crate::model::LoadStatus::CANCELED => 4,
|
||||||
crate::model::LoadStatus::ERROR => 5,
|
crate::model::LoadStatus::ERROR => 5,
|
||||||
};
|
};
|
||||||
vm.push(Value::Int64(status_val));
|
ret.push_int(status_val);
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::AssetCommit => {
|
Syscall::AssetCommit => {
|
||||||
let handle = vm.pop_integer()? as u32;
|
let handle = expect_int(args, 0)? as u32;
|
||||||
hw.assets().commit(handle);
|
hw.assets().commit(handle);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::AssetCancel => {
|
Syscall::AssetCancel => {
|
||||||
let handle = vm.pop_integer()? as u32;
|
let handle = expect_int(args, 0)? as u32;
|
||||||
hw.assets().cancel(handle);
|
hw.assets().cancel(handle);
|
||||||
vm.push(Value::Null);
|
ret.push_null();
|
||||||
Ok(100)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::BankInfo => {
|
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 {
|
let asset_type = match asset_type_val {
|
||||||
0 => crate::model::BankType::TILES,
|
0 => crate::model::BankType::TILES,
|
||||||
1 => crate::model::BankType::SOUNDS,
|
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 info = hw.assets().bank_info(asset_type);
|
||||||
let json = serde_json::to_string(&info).unwrap_or_default();
|
let json = serde_json::to_string(&info).unwrap_or_default();
|
||||||
vm.push(Value::String(json));
|
ret.push_string(json);
|
||||||
Ok(500)
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::BankSlotInfo => {
|
Syscall::BankSlotInfo => {
|
||||||
let slot_index = vm.pop_integer()? as usize;
|
let asset_type_val = expect_int(args, 0)? as u32;
|
||||||
let asset_type_val = vm.pop_integer()? as u32;
|
let slot_index = expect_int(args, 1)? as usize;
|
||||||
let asset_type = match asset_type_val {
|
let asset_type = match asset_type_val {
|
||||||
0 => crate::model::BankType::TILES,
|
0 => crate::model::BankType::TILES,
|
||||||
1 => crate::model::BankType::SOUNDS,
|
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 slot = crate::model::SlotRef { asset_type, index: slot_index };
|
||||||
let info = hw.assets().slot_info(slot);
|
let info = hw.assets().slot_info(slot);
|
||||||
let json = serde_json::to_string(&info).unwrap_or_default();
|
let json = serde_json::to_string(&info).unwrap_or_default();
|
||||||
vm.push(Value::String(json));
|
ret.push_string(json);
|
||||||
Ok(500)
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,14 +9,79 @@ pub use program::Program;
|
|||||||
pub use prometeu_bytecode::opcode::OpCode;
|
pub use prometeu_bytecode::opcode::OpCode;
|
||||||
pub use value::Value;
|
pub use value::Value;
|
||||||
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
|
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 {
|
pub trait NativeInterface {
|
||||||
/// Dispatches a syscall from the Virtual Machine to the native implementation.
|
/// 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.
|
/// ABI Rule: Arguments for the syscall are passed in `args`.
|
||||||
/// Since the stack is LIFO, the last argument of the call is the first to be popped.
|
|
||||||
///
|
///
|
||||||
/// The implementation MUST pop all its arguments and SHOULD push a return value if the
|
/// Returns are written via `ret`.
|
||||||
/// syscall is defined to return one.
|
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, hw: &mut dyn HardwareBridge) -> Result<(), VmFault>;
|
||||||
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<u64, String>;
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ pub enum Value {
|
|||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
/// UTF-8 string. Strings are immutable and usually come from the Constant Pool.
|
/// UTF-8 string. Strings are immutable and usually come from the Constant Pool.
|
||||||
String(String),
|
String(String),
|
||||||
|
/// Bounded 16-bit-ish integer.
|
||||||
|
Bounded(u32),
|
||||||
/// A pointer to an object on the heap.
|
/// A pointer to an object on the heap.
|
||||||
Gate(usize),
|
Gate(usize),
|
||||||
/// Represents the absence of a value (equivalent to `null` or `undefined`).
|
/// 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::Float(a), Value::Int64(b)) => *a == *b as f64,
|
||||||
(Value::Boolean(a), Value::Boolean(b)) => a == b,
|
(Value::Boolean(a), Value::Boolean(b)) => a == b,
|
||||||
(Value::String(a), Value::String(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::Gate(a), Value::Gate(b)) => a == b,
|
||||||
(Value::Null, Value::Null) => true,
|
(Value::Null, Value::Null) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
@ -55,6 +58,7 @@ impl PartialOrd for Value {
|
|||||||
(Value::Int32(a), Value::Int64(b)) => (*a as i64).partial_cmp(b),
|
(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::Int64(a), Value::Int32(b)) => a.partial_cmp(&(*b as i64)),
|
||||||
(Value::Float(a), Value::Float(b)) => a.partial_cmp(b),
|
(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::Int32(a), Value::Float(b)) => (*a as f64).partial_cmp(b),
|
||||||
(Value::Float(a), Value::Int32(b)) => a.partial_cmp(&(*b as f64)),
|
(Value::Float(a), Value::Int32(b)) => a.partial_cmp(&(*b as f64)),
|
||||||
(Value::Int64(a), Value::Float(b)) => (*a as f64).partial_cmp(b),
|
(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::Int32(i) => Some(*i as f64),
|
||||||
Value::Int64(i) => Some(*i as f64),
|
Value::Int64(i) => Some(*i as f64),
|
||||||
Value::Float(f) => Some(*f),
|
Value::Float(f) => Some(*f),
|
||||||
|
Value::Bounded(b) => Some(*b as f64),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,6 +86,7 @@ impl Value {
|
|||||||
Value::Int32(i) => Some(*i as i64),
|
Value::Int32(i) => Some(*i as i64),
|
||||||
Value::Int64(i) => Some(*i),
|
Value::Int64(i) => Some(*i),
|
||||||
Value::Float(f) => Some(*f as i64),
|
Value::Float(f) => Some(*f as i64),
|
||||||
|
Value::Bounded(b) => Some(*b as i64),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,6 +96,7 @@ impl Value {
|
|||||||
Value::Int32(i) => i.to_string(),
|
Value::Int32(i) => i.to_string(),
|
||||||
Value::Int64(i) => i.to_string(),
|
Value::Int64(i) => i.to_string(),
|
||||||
Value::Float(f) => f.to_string(),
|
Value::Float(f) => f.to_string(),
|
||||||
|
Value::Bounded(b) => format!("{}b", b),
|
||||||
Value::Boolean(b) => b.to_string(),
|
Value::Boolean(b) => b.to_string(),
|
||||||
Value::String(s) => s.clone(),
|
Value::String(s) => s.clone(),
|
||||||
Value::Gate(r) => format!("[Gate {}]", r),
|
Value::Gate(r) => format!("[Gate {}]", r),
|
||||||
|
|||||||
@ -634,9 +634,45 @@ impl VirtualMachine {
|
|||||||
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
}
|
}
|
||||||
OpCode::Syscall => {
|
OpCode::Syscall => {
|
||||||
|
let pc_at_syscall = start_pc as u32;
|
||||||
|
|
||||||
let id = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
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 => {
|
OpCode::FrameSync => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -758,12 +794,12 @@ impl VirtualMachine {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::hardware::HardwareBridge;
|
use crate::hardware::HardwareBridge;
|
||||||
use crate::virtual_machine::Value;
|
use crate::virtual_machine::{Value, HostReturn, VmFault, expect_int};
|
||||||
|
|
||||||
struct MockNative;
|
struct MockNative;
|
||||||
impl NativeInterface for MockNative {
|
impl NativeInterface for MockNative {
|
||||||
fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine, _hw: &mut dyn HardwareBridge) -> Result<u64, String> {
|
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> {
|
||||||
Ok(0)
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1314,7 +1350,7 @@ mod tests {
|
|||||||
let mut hw = crate::Hardware::new();
|
let mut hw = crate::Hardware::new();
|
||||||
struct TestNative;
|
struct TestNative;
|
||||||
impl NativeInterface for 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;
|
let mut native = TestNative;
|
||||||
|
|
||||||
@ -1322,4 +1358,156 @@ mod tests {
|
|||||||
let result = vm.run_budget(100, &mut native, &mut hw).expect("VM run failed");
|
let result = vm.run_budget(100, &mut native, &mut hw).expect("VM run failed");
|
||||||
assert_eq!(result.reason, LogicalFrameEndingReason::EndOfRom);
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,28 @@
|
|||||||
> **Hard constraints:**
|
> **Status:** Ready to copy/paste to Junie
|
||||||
>
|
>
|
||||||
> * `ir_core` and `ir_vm` remain **fully decoupled**.
|
> **Goal:** expose hardware types **1:1** to PBS and VM as **SAFE builtins** (stack/value), *not* HIP.
|
||||||
> * The only contact point is lowering (`core_to_vm`).
|
>
|
||||||
> * **No placeholders**, no guessed offsets, no runtime inference of language semantics.
|
> **Key constraint:** Prometeu does **not** have `u16` as a primitive. Use `bounded` for 16-bit-ish hardware scalars.
|
||||||
> * Every PR must include tests.
|
>
|
||||||
|
> **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.
|
||||||
|
|||||||
@ -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
|
### 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
|
```pbs
|
||||||
* mutates a field
|
pub declare struct Color(value: bounded)
|
||||||
* peeks value
|
[
|
||||||
|
(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)`
|
pub declare struct Touch(
|
||||||
* `GateBeginMutate/EndMutate`
|
f: ButtonState,
|
||||||
* `GateStore(offset)`
|
x: int,
|
||||||
* `GateBeginPeek/EndPeek`
|
y: int
|
||||||
* `GateLoad(offset)`
|
)
|
||||||
* RC ops (retain/release)
|
```
|
||||||
|
|
||||||
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
|
### 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
|
### Goal
|
||||||
* RC events are explicit and testable
|
|
||||||
* HIP ISA/ABI v0 is frozen with golden bytecode tests
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user