This commit is contained in:
bQUARKz 2026-02-19 18:30:01 +00:00
parent b9723cfc40
commit a7ddbcabb2
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 427 additions and 311 deletions

View File

@ -129,6 +129,154 @@ pub enum Syscall {
BankSlotInfo = 0x6102,
}
/// Canonical metadata describing a syscall using the unified slot-based ABI.
///
/// This structure is the single source of truth for:
/// - Argument slot count (inputs pulled from the VM stack)
/// - Return slot count (values pushed back to the VM stack)
/// - Capability flags (what permission is required to call this syscall)
/// - Determinism characteristics (if known/defined by the spec)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyscallMeta {
/// Numeric identifier of the syscall (matches the enum discriminant).
pub id: u32,
/// Number of input slots consumed from the VM stack.
pub arg_slots: u8,
/// Number of output slots produced onto the VM stack.
pub ret_slots: u16,
/// Capability flags required for this syscall.
pub caps: CapFlags,
/// Determinism characteristics for the syscall.
pub determinism: Determinism,
}
/// Bitflags representing capabilities required to invoke a syscall.
///
/// This avoids adding a new dependency; flags are represented in a plain
/// `u64` and combined via bitwise OR. Extend as needed as the capability
/// model evolves.
pub type CapFlags = u64;
pub mod caps {
use super::CapFlags;
pub const NONE: CapFlags = 0;
pub const SYSTEM: CapFlags = 1 << 0;
pub const GFX: CapFlags = 1 << 1;
pub const INPUT: CapFlags = 1 << 2;
pub const AUDIO: CapFlags = 1 << 3;
pub const FS: CapFlags = 1 << 4;
pub const LOG: CapFlags = 1 << 5;
pub const ASSET: CapFlags = 1 << 6;
pub const BANK: CapFlags = 1 << 7;
}
/// Determinism flags for a syscall.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Determinism {
/// Determinism is not specified in the current spec.
Unknown,
/// Given the same VM state and inputs, result is deterministic.
Deterministic,
/// May vary across runs (e.g., time, external IO race), even with same inputs.
NonDeterministic,
}
/// Pairing of a strongly-typed syscall and its metadata.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyscallRegistryEntry {
pub syscall: Syscall,
pub meta: SyscallMeta,
}
/// Canonical registry of all syscalls and their metadata.
///
/// IMPORTANT: This table is the single authoritative source for the slot-based
/// ABI. All helper methods (e.g., `args_count`/`results_count`) must read from
/// this table.
pub const SYSCALL_TABLE: &[SyscallRegistryEntry] = &[
// --- System ---
SyscallRegistryEntry { syscall: Syscall::SystemHasCart, meta: SyscallMeta { id: 0x0001, arg_slots: 0, ret_slots: 1, caps: caps::SYSTEM, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::SystemRunCart, meta: SyscallMeta { id: 0x0002, arg_slots: 0, ret_slots: 0, caps: caps::SYSTEM, determinism: Determinism::NonDeterministic } },
// --- GFX ---
SyscallRegistryEntry { syscall: Syscall::GfxClear, meta: SyscallMeta { id: 0x1001, arg_slots: 1, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxFillRect, meta: SyscallMeta { id: 0x1002, arg_slots: 5, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawLine, meta: SyscallMeta { id: 0x1003, arg_slots: 5, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawCircle, meta: SyscallMeta { id: 0x1004, arg_slots: 4, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawDisc, meta: SyscallMeta { id: 0x1005, arg_slots: 5, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawSquare, meta: SyscallMeta { id: 0x1006, arg_slots: 6, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxSetSprite, meta: SyscallMeta { id: 0x1007, arg_slots: 10, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawText, meta: SyscallMeta { id: 0x1008, arg_slots: 4, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::GfxClear565, meta: SyscallMeta { id: 0x1010, arg_slots: 1, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic } },
// --- Input ---
SyscallRegistryEntry { syscall: Syscall::InputGetPad, meta: SyscallMeta { id: 0x2001, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::InputGetPadPressed, meta: SyscallMeta { id: 0x2002, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::InputGetPadReleased, meta: SyscallMeta { id: 0x2003, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::InputGetPadHold, meta: SyscallMeta { id: 0x2004, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::InputPadSnapshot, meta: SyscallMeta { id: 0x2010, arg_slots: 0, ret_slots: 48, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::InputTouchSnapshot, meta: SyscallMeta { id: 0x2011, arg_slots: 0, ret_slots: 6, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchGetX, meta: SyscallMeta { id: 0x2101, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchGetY, meta: SyscallMeta { id: 0x2102, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchIsDown, meta: SyscallMeta { id: 0x2103, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchIsPressed, meta: SyscallMeta { id: 0x2104, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchIsReleased, meta: SyscallMeta { id: 0x2105, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchGetHold, meta: SyscallMeta { id: 0x2106, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::TouchGetFinger, meta: SyscallMeta { id: 0x2107, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
// --- Input (Pad service-based) ---
SyscallRegistryEntry { syscall: Syscall::PadGetUp, meta: SyscallMeta { id: 0x2200, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetDown, meta: SyscallMeta { id: 0x2201, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetLeft, meta: SyscallMeta { id: 0x2202, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetRight, meta: SyscallMeta { id: 0x2203, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetA, meta: SyscallMeta { id: 0x2204, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetB, meta: SyscallMeta { id: 0x2205, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetX, meta: SyscallMeta { id: 0x2206, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetY, meta: SyscallMeta { id: 0x2207, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetL, meta: SyscallMeta { id: 0x2208, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetR, meta: SyscallMeta { id: 0x2209, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetStart, meta: SyscallMeta { id: 0x220A, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::PadGetSelect, meta: SyscallMeta { id: 0x220B, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic } },
// --- Audio ---
SyscallRegistryEntry { syscall: Syscall::AudioPlaySample, meta: SyscallMeta { id: 0x3001, arg_slots: 5, ret_slots: 0, caps: caps::AUDIO, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::AudioPlay, meta: SyscallMeta { id: 0x3002, arg_slots: 7, ret_slots: 0, caps: caps::AUDIO, determinism: Determinism::Deterministic } },
// --- FS ---
SyscallRegistryEntry { syscall: Syscall::FsOpen, meta: SyscallMeta { id: 0x4001, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::FsRead, meta: SyscallMeta { id: 0x4002, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::FsWrite, meta: SyscallMeta { id: 0x4003, arg_slots: 2, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::FsClose, meta: SyscallMeta { id: 0x4004, arg_slots: 1, ret_slots: 0, caps: caps::FS, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::FsListDir, meta: SyscallMeta { id: 0x4005, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::FsExists, meta: SyscallMeta { id: 0x4006, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::FsDelete, meta: SyscallMeta { id: 0x4007, arg_slots: 1, ret_slots: 0, caps: caps::FS, determinism: Determinism::NonDeterministic } },
// --- Log ---
SyscallRegistryEntry { syscall: Syscall::LogWrite, meta: SyscallMeta { id: 0x5001, arg_slots: 2, ret_slots: 0, caps: caps::LOG, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::LogWriteTag, meta: SyscallMeta { id: 0x5002, arg_slots: 3, ret_slots: 0, caps: caps::LOG, determinism: Determinism::NonDeterministic } },
// --- Asset/Bank ---
SyscallRegistryEntry { syscall: Syscall::AssetLoad, meta: SyscallMeta { id: 0x6001, arg_slots: 3, ret_slots: 1, caps: caps::ASSET, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::AssetStatus, meta: SyscallMeta { id: 0x6002, arg_slots: 1, ret_slots: 1, caps: caps::ASSET, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::AssetCommit, meta: SyscallMeta { id: 0x6003, arg_slots: 1, ret_slots: 0, caps: caps::ASSET, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::AssetCancel, meta: SyscallMeta { id: 0x6004, arg_slots: 1, ret_slots: 0, caps: caps::ASSET, determinism: Determinism::NonDeterministic } },
SyscallRegistryEntry { syscall: Syscall::BankInfo, meta: SyscallMeta { id: 0x6101, arg_slots: 1, ret_slots: 1, caps: caps::BANK, determinism: Determinism::Deterministic } },
SyscallRegistryEntry { syscall: Syscall::BankSlotInfo, meta: SyscallMeta { id: 0x6102, arg_slots: 2, ret_slots: 1, caps: caps::BANK, determinism: Determinism::Deterministic } },
];
/// Returns the metadata associated with this syscall.
pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
// Linear scan is acceptable given the very small number of syscalls.
// If this grows substantially, replace with a perfect hash or match.
for entry in SYSCALL_TABLE {
if entry.syscall == syscall {
return &entry.meta;
}
}
panic!("Missing SyscallMeta for {:?}", syscall);
}
impl Syscall {
pub fn from_u32(id: u32) -> Option<Self> {
match id {
@ -190,135 +338,11 @@ impl Syscall {
}
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::GfxClear565 => 1,
Self::InputGetPad => 1,
Self::InputGetPadPressed => 1,
Self::InputGetPadReleased => 1,
Self::InputGetPadHold => 1,
Self::InputPadSnapshot => 0,
Self::InputTouchSnapshot => 0,
Self::TouchGetX => 0,
Self::TouchGetY => 0,
Self::TouchIsDown => 0,
Self::TouchIsPressed => 0,
Self::TouchIsReleased => 0,
Self::TouchGetHold => 0,
Self::TouchGetFinger => 0,
Self::PadGetUp => 0,
Self::PadGetDown => 0,
Self::PadGetLeft => 0,
Self::PadGetRight => 0,
Self::PadGetA => 0,
Self::PadGetB => 0,
Self::PadGetX => 0,
Self::PadGetY => 0,
Self::PadGetL => 0,
Self::PadGetR => 0,
Self::PadGetStart => 0,
Self::PadGetSelect => 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,
}
meta_for(*self).arg_slots as usize
}
pub fn results_count(&self) -> usize {
match self {
// --- System ---
Self::SystemHasCart => 1,
Self::SystemRunCart => 0,
// --- GFX (void) ---
Self::GfxClear => 0,
Self::GfxFillRect => 0,
Self::GfxDrawLine => 0,
Self::GfxDrawCircle => 0,
Self::GfxDrawDisc => 0,
Self::GfxDrawSquare => 0,
Self::GfxSetSprite => 0,
Self::GfxDrawText => 0,
Self::GfxClear565 => 0,
// --- Input (scalar/snapshots) ---
Self::InputGetPad => 1,
Self::InputGetPadPressed => 1,
Self::InputGetPadReleased => 1,
Self::InputGetPadHold => 1,
Self::InputPadSnapshot => 48,
Self::InputTouchSnapshot => 6,
// --- Touch (scalars/struct) ---
Self::TouchGetX => 1,
Self::TouchGetY => 1,
Self::TouchIsDown => 1,
Self::TouchIsPressed => 1,
Self::TouchIsReleased => 1,
Self::TouchGetHold => 1,
Self::TouchGetFinger => 4, // Button struct (4 slots)
// --- Pad (per-button struct: 4 slots) ---
Self::PadGetUp
| Self::PadGetDown
| Self::PadGetLeft
| Self::PadGetRight
| Self::PadGetA
| Self::PadGetB
| Self::PadGetX
| Self::PadGetY
| Self::PadGetL
| Self::PadGetR
| Self::PadGetStart
| Self::PadGetSelect => 4,
// --- Audio (void) ---
Self::AudioPlaySample => 0,
Self::AudioPlay => 0,
// --- FS ---
Self::FsOpen => 1,
Self::FsRead => 1, // bytes read
Self::FsWrite => 1, // bytes written
Self::FsClose => 0,
Self::FsListDir => 1, // entries count/handle (TBD)
Self::FsExists => 1,
Self::FsDelete => 0,
// --- Log (void) ---
Self::LogWrite | Self::LogWriteTag => 0,
// --- Asset/Bank (conservador) ---
Self::AssetLoad => 1,
Self::AssetStatus => 1,
Self::AssetCommit => 0,
Self::AssetCancel => 0,
Self::BankInfo => 1,
Self::BankSlotInfo => 1,
}
meta_for(*self).ret_slots as usize
}
pub fn name(&self) -> &'static str {
@ -379,3 +403,95 @@ impl Syscall {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all_syscalls() -> &'static [Syscall] {
&[
// System
Syscall::SystemHasCart,
Syscall::SystemRunCart,
// GFX
Syscall::GfxClear,
Syscall::GfxFillRect,
Syscall::GfxDrawLine,
Syscall::GfxDrawCircle,
Syscall::GfxDrawDisc,
Syscall::GfxDrawSquare,
Syscall::GfxSetSprite,
Syscall::GfxDrawText,
Syscall::GfxClear565,
// Input
Syscall::InputGetPad,
Syscall::InputGetPadPressed,
Syscall::InputGetPadReleased,
Syscall::InputGetPadHold,
Syscall::InputPadSnapshot,
Syscall::InputTouchSnapshot,
Syscall::TouchGetX,
Syscall::TouchGetY,
Syscall::TouchIsDown,
Syscall::TouchIsPressed,
Syscall::TouchIsReleased,
Syscall::TouchGetHold,
Syscall::TouchGetFinger,
// Pad service
Syscall::PadGetUp,
Syscall::PadGetDown,
Syscall::PadGetLeft,
Syscall::PadGetRight,
Syscall::PadGetA,
Syscall::PadGetB,
Syscall::PadGetX,
Syscall::PadGetY,
Syscall::PadGetL,
Syscall::PadGetR,
Syscall::PadGetStart,
Syscall::PadGetSelect,
// Audio
Syscall::AudioPlaySample,
Syscall::AudioPlay,
// FS
Syscall::FsOpen,
Syscall::FsRead,
Syscall::FsWrite,
Syscall::FsClose,
Syscall::FsListDir,
Syscall::FsExists,
Syscall::FsDelete,
// Log
Syscall::LogWrite,
Syscall::LogWriteTag,
// Asset/Bank
Syscall::AssetLoad,
Syscall::AssetStatus,
Syscall::AssetCommit,
Syscall::AssetCancel,
Syscall::BankInfo,
Syscall::BankSlotInfo,
]
}
#[test]
fn every_syscall_has_metadata() {
// 1) Every enum variant must appear in the table.
for sc in all_syscalls() {
let m = meta_for(*sc);
assert_eq!(m.id, *sc as u32, "id mismatch for {:?}", sc);
}
// 2) Table must not contain duplicates and must map back to a valid enum.
use std::collections::HashSet;
let mut ids = HashSet::new();
for e in SYSCALL_TABLE {
assert!(ids.insert(e.meta.id), "duplicate syscall id 0x{:08X}", e.meta.id);
let parsed = Syscall::from_u32(e.meta.id).expect("id not recognized by enum mapping");
assert_eq!(parsed as u32, e.meta.id);
}
// 3) Table and explicit list sizes must match (guard against omissions).
assert_eq!(SYSCALL_TABLE.len(), all_syscalls().len());
}
}

View File

@ -80,6 +80,9 @@ pub struct VirtualMachine {
pub gc_alloc_threshold: usize,
/// GC: snapshot of live objects count after the last collection (or VM init).
last_gc_live_count: usize,
/// Capability flags granted to the currently running program/cart.
/// Syscalls are capability-gated using `prometeu_hal::syscalls::SyscallMeta::caps`.
pub capabilities: prometeu_hal::syscalls::CapFlags,
}
@ -110,6 +113,9 @@ impl VirtualMachine {
breakpoints: std::collections::HashSet::new(),
gc_alloc_threshold: 1024, // conservative default; tests may override
last_gc_live_count: 0,
// Default to all capabilities allowed for backward compatibility.
// Tests can narrow this via `set_capabilities`.
capabilities: u64::MAX,
}
}
@ -131,6 +137,7 @@ impl VirtualMachine {
self.cycles = 0;
self.halted = true; // execution is impossible until a successful load
self.last_gc_live_count = 0;
// Preserve capabilities across loads; firmware may set them per cart.
// Only recognized format is loadable: PBS v0 industrial format
let program = if program_bytes.starts_with(b"PBS\0") {
@ -191,6 +198,11 @@ impl VirtualMachine {
Ok(())
}
/// Sets the capability flags for the current program.
pub fn set_capabilities(&mut self, caps: prometeu_hal::syscalls::CapFlags) {
self.capabilities = caps;
}
/// Prepares the VM to execute a specific entrypoint by setting the PC and
/// pushing an initial call frame.
pub fn prepare_call(&mut self, entrypoint: &str) {
@ -912,6 +924,20 @@ impl VirtualMachine {
)
})?;
// Capability check before any side effects or argument consumption.
let meta = prometeu_hal::syscalls::meta_for(syscall);
if (self.capabilities & meta.caps) != meta.caps {
return Err(self.trap(
TRAP_INVALID_SYSCALL,
OpCode::Syscall as u16,
format!(
"Missing capability for syscall {} (required=0x{:X})",
syscall.name(), meta.caps
),
pc_at_syscall,
));
}
let args_count = syscall.args_count();
let mut args = Vec::with_capacity(args_count);
@ -1846,6 +1872,59 @@ mod tests {
}
}
#[test]
fn test_syscall_missing_capability_trap() {
// Program: directly call GfxClear (0x1001). We check caps before args, so no underflow.
let rom = vec![
0x70, 0x00, // Syscall + Reserved
0x01, 0x10, 0x00, 0x00, // Syscall ID 0x1001 (LE)
];
let mut vm = new_test_vm(rom.clone(), vec![]);
// Remove all capabilities
vm.set_capabilities(0);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, TRAP_INVALID_SYSCALL);
assert_eq!(trap.opcode, OpCode::Syscall as u16);
assert!(trap.message.contains("Missing capability"));
assert_eq!(trap.pc, 0);
}
other => panic!("Expected Trap, got {:?}", other),
}
}
#[test]
fn test_syscall_with_capability_success() {
// Program: push arg 0; call GfxClear (0x1001)
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&0i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
rom.extend_from_slice(&0x1001u32.to_le_bytes()); // GfxClear
let mut vm = new_test_vm(rom.clone(), vec![]);
// Grant only GFX capability
vm.set_capabilities(prometeu_hal::syscalls::caps::GFX);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
// Any non-trap outcome is considered success here
match report.reason {
LogicalFrameEndingReason::Trap(trap) => panic!("Unexpected trap: {:?}", trap),
_ => {}
}
}
#[test]
fn test_syscall_results_count_mismatch_panic() {
// GfxClear565 (0x1010) expects 0 results

View File

@ -2,7 +2,7 @@
# **Host ABI and Syscalls**
This chapter defines the Application Binary Interface (ABI) between the Prometeu Virtual Machine (PVM) and the host environment. It specifies how syscalls are encoded, invoked, verified, and accounted for.
This chapter defines the Application Binary Interface (ABI) between the Prometeu Virtual Machine (PVM) and the host environment. It specifies how syscalls are identified, resolved, invoked, verified, and accounted for.
Syscalls provide controlled access to host-managed subsystems such as graphics, audio, input, asset banks, and persistent storage.
@ -20,10 +20,89 @@ The syscall system follows these rules:
4. **Capability-gated**: Each syscall requires a declared capability.
5. **Stack-based ABI**: Arguments and return values are passed via VM slots.
6. **Not first-class**: Syscalls are callable but cannot be stored as values.
7. **Language-agnostic identity**: Syscalls are identified by canonical names, not language syntax.
---
## 2 Syscall Instruction Semantics
## 2 Canonical Syscall Identity
Syscalls are identified by a **canonical triple**:
```
(module, name, version)
```
Example:
```
("gfx", "present", 1)
("input", "state", 1)
("audio", "play", 2)
```
This identity is:
* Independent of language syntax.
* Stable across languages and toolchains.
* Used for capability checks and linking.
### Language independence
Different languages may reference the same syscall using different syntax:
| Language style | Reference form |
| -------------- | --------------- |
| Dot-based | `gfx.present` |
| Namespace | `gfx::present` |
| Object-style | `Gfx.present()` |
| Functional | `present(gfx)` |
All of these map to the same canonical identity:
```
("gfx", "present", 1)
```
The compiler is responsible for this mapping.
---
## 3 Syscall Resolution
The host maintains a **Syscall Registry**:
```
(module, name, version) -> syscall_id
```
Example:
```
("gfx", "present", 1) -> 0x0101
("gfx", "submit", 1) -> 0x0102
("audio", "play", 1) -> 0x0201
```
### Resolution process
At cartridge load time:
1. The cartridge declares required syscalls using canonical identities.
2. The host verifies capabilities.
3. The host resolves each canonical identity to a numeric `syscall_id`.
4. The VM stores the resolved table for fast execution.
At runtime, the VM executes:
```
SYSCALL <id>
```
Only numeric IDs are used during execution.
---
## 4 Syscall Instruction Semantics
The VM provides a single instruction:
@ -37,7 +116,7 @@ Where:
Execution steps:
1. The VM looks up the syscall metadata using `<id>`.
1. The VM looks up syscall metadata using `<id>`.
2. The VM verifies that enough arguments exist on the stack.
3. The VM checks capability requirements.
4. The syscall executes in the host environment.
@ -47,7 +126,7 @@ If any contract rule is violated, the VM traps.
---
## 3 Syscall Metadata Table
## 5 Syscall Metadata Table
Each syscall is defined by a metadata entry.
@ -56,7 +135,9 @@ Each syscall is defined by a metadata entry.
```
SyscallMeta {
id: u32
module: string
name: string
version: u16
arg_slots: u8
ret_slots: u8
capability: CapabilityId
@ -69,19 +150,21 @@ Fields:
| Field | Description |
| -------------- | ------------------------------------------------ |
| `id` | Unique syscall identifier |
| `name` | Human-readable name |
| `id` | Unique numeric syscall identifier |
| `module` | Canonical module name |
| `name` | Canonical syscall name |
| `version` | ABI version of the syscall |
| `arg_slots` | Number of input stack slots |
| `ret_slots` | Number of return stack slots |
| `capability` | Required capability |
| `may_allocate` | Whether the syscall may allocate VM heap objects |
| `cost_hint` | Expected cycle cost (for analysis/profiling) |
| `cost_hint` | Expected cycle cost |
The verifier uses this table to validate stack effects.
---
## 4 Arguments and Return Values
## 6 Arguments and Return Values
Syscalls use the same slot-based ABI as functions.
@ -109,21 +192,11 @@ SYSCALL input_state
// after: [held, pressed, released]
```
### Slot types
Each slot contains one of the VM value types:
* int
* bool
* float
* handle
* null
Composite return values are represented as multiple slots (stack tuples).
---
## 5 Syscalls as Callable Entities (Not First-Class)
## 7 Syscalls as Callable Entities (Not First-Class)
Syscalls behave like functions in terms of arguments and return values, but they are **not first-class values**.
@ -136,7 +209,7 @@ This means:
Only user-defined functions and closures are first-class.
### Example declaration (conceptual)
### Conceptual declaration
```
host fn input_state() -> (int, int, int)
@ -146,7 +219,7 @@ This represents a syscall with three return values, but it cannot be treated as
---
## 6 Error Model: Traps vs Status Codes
## 8 Error Model: Traps vs Status Codes
Syscalls use a hybrid error model.
@ -155,11 +228,12 @@ Syscalls use a hybrid error model.
The VM traps when:
* The syscall id is invalid.
* The canonical identity cannot be resolved.
* The required capability is missing.
* The stack does not contain enough arguments.
* A handle is invalid or dead.
These are considered fatal contract violations.
These are fatal contract violations.
### Status returns (domain conditions)
@ -175,7 +249,7 @@ These are represented by status codes in return slots.
---
## 7 Capability System
## 9 Capability System
Each syscall requires a capability.
@ -195,7 +269,7 @@ If a syscall is invoked without the required capability:
---
## 8 Interaction with the Garbage Collector
## 10 Interaction with the Garbage Collector
The VM heap is managed by the GC. Host-managed memory is separate.
@ -220,7 +294,7 @@ This rule applies only to VM heap objects (such as closures or user objects), no
---
## 9 Determinism Rules
## 11 Determinism Rules
Syscalls must obey deterministic execution rules.
@ -238,7 +312,7 @@ Allowed patterns:
---
## 10 Cost Model and Budgeting
## 12 Cost Model and Budgeting
Each syscall contributes to frame cost.
@ -261,7 +335,7 @@ Nothing is free.
---
## 11 Blocking and Long Operations
## 13 Blocking and Long Operations
Syscalls must not block.
@ -280,15 +354,19 @@ status, progress = asset.status(id)
---
## 12 Summary
## 14 Summary
* Syscalls are deterministic, synchronous, and non-blocking.
* They use the same slot-based ABI as functions.
* They are callable but not first-class.
* They are identified canonically by `(module, name, version)`.
* Language syntax does not affect the ABI.
* The host resolves canonical identities to numeric syscall IDs.
* Capabilities control access to host subsystems.
* GC only manages VM heap objects.
* Host-held heap objects must be registered as roots.
* All syscall costs are tracked per frame.
< [Back](chapter-15.md) | [Summary](table-of-contents.md) >

View File

@ -1,160 +1,3 @@
# PR-5.1 — Define Canonical Syscall Metadata Table
### Briefing
Syscalls must follow a unified, function-like ABI based on slot counts and capabilities. This PR introduces the canonical metadata table describing every syscall.
### Target
* Define a single authoritative metadata structure for syscalls.
* Ensure all syscalls are described in terms of slot-based ABI.
### Work items
* Introduce a `SyscallMeta` struct containing:
* Syscall identifier.
* `arg_slots`.
* `ret_slots`.
* Capability flags.
* Determinism flags (if defined in spec).
* Create a canonical syscall table/registry.
* Ensure all existing syscalls are registered in this table.
* Document the structure in code comments (English).
### Acceptance checklist
* [ ] All syscalls have a `SyscallMeta` entry.
* [ ] Metadata includes arg and return slot counts.
* [ ] No syscall is invoked without metadata.
* [ ] `cargo test` passes.
### Tests
* Add a test ensuring all registered syscalls have metadata.
### Junie instructions
**You MAY:**
* Introduce a syscall metadata struct.
* Add a central registry for syscalls.
**You MUST NOT:**
* Change existing syscall semantics.
* Add new syscalls.
**If unclear:**
* Ask before defining metadata fields.
---
# PR-5.2 — Implement Slot-Based Syscall Calling Convention
### Briefing
Syscalls must behave like functions using the stack-based slot ABI. This PR implements the unified calling convention.
### Target
* Ensure syscalls read arguments from the stack.
* Ensure syscalls push return values based on `ret_slots`.
### Work items
* Modify the VM syscall dispatch logic to:
* Look up `SyscallMeta`.
* Pop `arg_slots` from the stack.
* Invoke the syscall.
* Push `ret_slots` results.
* Remove any legacy argument handling paths.
### Acceptance checklist
* [ ] All syscalls use slot-based calling convention.
* [ ] Argument and return slot counts are enforced.
* [ ] No legacy calling paths remain.
* [ ] `cargo test` passes.
### Tests
* Add tests:
* Syscall with correct arg/ret slots → passes.
* Syscall with insufficient args → trap or verifier failure.
### Junie instructions
**You MAY:**
* Refactor syscall dispatch code.
* Use metadata to enforce slot counts.
**You MUST NOT:**
* Change the stack model.
* Add compatibility layers.
**If unclear:**
* Ask before modifying dispatch semantics.
---
# PR-5.3 — Enforce Capability Checks in Syscall Dispatch
### Briefing
Syscalls must be capability-gated. The runtime must verify that the current program has permission to invoke each syscall.
### Target
* Enforce capability checks before syscall execution.
### Work items
* Extend `SyscallMeta` with capability requirements.
* Add capability data to the VM or execution context.
* In syscall dispatch:
* Check capabilities.
* If missing, trigger `TRAP_INVALID_SYSCALL` or equivalent.
### Acceptance checklist
* [ ] Capability checks occur before syscall execution.
* [ ] Missing capability leads to deterministic trap.
* [ ] Valid capabilities allow execution.
* [ ] `cargo test` passes.
### Tests
* Add tests:
* Syscall without capability → trap.
* Syscall with capability → success.
### Junie instructions
**You MAY:**
* Add capability checks in dispatch.
* Extend metadata with capability fields.
**You MUST NOT:**
* Change trap semantics.
* Introduce dynamic or nondeterministic permission systems.
**If unclear:**
* Ask before defining capability behavior.
---
# PR-5.4 — Verifier Integration for Syscall Slot Rules
### Briefing