From 9a60754e5a8a304af3604132f2627e71c985aff0 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 3 Mar 2026 16:42:23 +0000 Subject: [PATCH] break monster VM into small pieces :D --- crates/console/prometeu-hal/src/syscalls.rs | 1471 +---------------- .../console/prometeu-hal/src/syscalls/caps.rs | 12 + .../src/syscalls/domains/asset.rs | 53 + .../src/syscalls/domains/audio.rs | 29 + .../prometeu-hal/src/syscalls/domains/bank.rs | 29 + .../prometeu-hal/src/syscalls/domains/fs.rs | 78 + .../prometeu-hal/src/syscalls/domains/gfx.rs | 113 ++ .../src/syscalls/domains/input.rs | 305 ++++ .../prometeu-hal/src/syscalls/domains/log.rs | 29 + .../prometeu-hal/src/syscalls/domains/mod.rs | 51 + .../src/syscalls/domains/system.rs | 29 + .../prometeu-hal/src/syscalls/registry.rs | 137 ++ .../prometeu-hal/src/syscalls/resolver.rs | 156 ++ .../prometeu-hal/src/syscalls/tests.rs | 169 ++ .../src/virtual_machine_runtime.rs | 1455 +--------------- .../src/virtual_machine_runtime/dispatch.rs | 546 ++++++ .../src/virtual_machine_runtime/lifecycle.rs | 145 ++ .../src/virtual_machine_runtime/tests.rs | 277 ++++ .../src/virtual_machine_runtime/tick.rs | 203 +++ .../prometeu-vm/src/virtual_machine.rs | 619 +------ .../prometeu-vm/src/virtual_machine/gc.rs | 115 ++ .../prometeu-vm/src/virtual_machine/loader.rs | 182 ++ .../src/virtual_machine/runtime.rs | 129 ++ .../prometeu-vm/src/virtual_machine/stack.rs | 76 + 24 files changed, 2894 insertions(+), 3514 deletions(-) create mode 100644 crates/console/prometeu-hal/src/syscalls/caps.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/asset.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/audio.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/bank.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/fs.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/gfx.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/input.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/log.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/mod.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/system.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/registry.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/resolver.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/tests.rs create mode 100644 crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs create mode 100644 crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs create mode 100644 crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs create mode 100644 crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs create mode 100644 crates/console/prometeu-vm/src/virtual_machine/gc.rs create mode 100644 crates/console/prometeu-vm/src/virtual_machine/loader.rs create mode 100644 crates/console/prometeu-vm/src/virtual_machine/runtime.rs create mode 100644 crates/console/prometeu-vm/src/virtual_machine/stack.rs diff --git a/crates/console/prometeu-hal/src/syscalls.rs b/crates/console/prometeu-hal/src/syscalls.rs index 91f2774b..2290c651 100644 --- a/crates/console/prometeu-hal/src/syscalls.rs +++ b/crates/console/prometeu-hal/src/syscalls.rs @@ -1,3 +1,16 @@ +mod domains; +mod registry; +mod resolver; +#[cfg(test)] +mod tests; + +pub mod caps; + +pub use resolver::{ + DeclaredLoadError, LoadError, SyscallIdentity, SyscallResolved, + resolve_declared_program_syscalls, resolve_program_syscalls, resolve_syscall, +}; + /// Enumeration of all System Calls (Syscalls) available in the Prometeu environment. /// /// Syscalls are the primary mechanism for a program running in the Virtual Machine @@ -14,63 +27,30 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u32)] pub enum Syscall { - // --- System --- - /// Checks if a cartridge is currently inserted in the virtual slot. SystemHasCart = 0x0001, - /// Requests the OS to launch a specific cartridge. SystemRunCart = 0x0002, - - // --- GFX (Graphics) --- - /// Fills the entire back buffer with a single color. GfxClear = 0x1001, - /// Draws a solid rectangle. GfxFillRect = 0x1002, - /// Draws a 1-pixel wide line. GfxDrawLine = 0x1003, - /// Draws a circle outline. GfxDrawCircle = 0x1004, - /// Draws a filled circle (disc). GfxDrawDisc = 0x1005, - /// Draws a rectangle outline. GfxDrawSquare = 0x1006, - /// Configures one of the 512 hardware sprites. GfxSetSprite = 0x1007, - /// Draws a text string at the specified coordinates. GfxDrawText = 0x1008, - /// Fills the entire back buffer with a single RGB565 color (flattened). GfxClear565 = 0x1010, - - // --- Input --- - /// Returns the current raw state of the digital gamepad (bitmask). InputGetPad = 0x2001, - /// Returns buttons that were pressed exactly in this frame. InputGetPadPressed = 0x2002, - /// Returns buttons that were released exactly in this frame. InputGetPadReleased = 0x2003, - /// Returns how many frames a button has been held down. InputGetPadHold = 0x2004, - /// Returns the full snapshot of the gamepad state (48 slots). InputPadSnapshot = 0x2010, - /// Returns the full snapshot of the touch state (6 slots). InputTouchSnapshot = 0x2011, - - /// Returns the X coordinate of the touch/mouse pointer. TouchGetX = 0x2101, - /// Returns the Y coordinate of the touch/mouse pointer. TouchGetY = 0x2102, - /// Returns true if the pointer is currently touching the screen. TouchIsDown = 0x2103, - /// Returns true if the touch started in this frame. TouchIsPressed = 0x2104, - /// Returns true if the touch ended in this frame. TouchIsReleased = 0x2105, - /// Returns how many frames the pointer has been held down. TouchGetHold = 0x2106, - /// Returns the full Button struct (6 bytes) for the touch finger state. TouchGetFinger = 0x2107, - - // --- Input (Pad service-based) --- - /// Returns the full Button struct (6 bytes) for each gamepad button PadGetUp = 0x2200, PadGetDown = 0x2201, PadGetLeft = 0x2202, @@ -83,112 +63,48 @@ pub enum Syscall { PadGetR = 0x2209, PadGetStart = 0x220A, PadGetSelect = 0x220B, - - // --- Audio --- - /// Starts playback of a sound sample by its Bank and ID. AudioPlaySample = 0x3001, - /// Low-level audio play command. AudioPlay = 0x3002, - - // --- FS (Filesystem) --- - /// Opens a file for reading or writing. Returns a File Handle (u32). FsOpen = 0x4001, - /// Reads data from an open file handle into the VM heap. FsRead = 0x4002, - /// Writes data from the VM heap into an open file handle. FsWrite = 0x4003, - /// Closes an open file handle. FsClose = 0x4004, - /// Lists entries in a directory. FsListDir = 0x4005, - /// Checks if a file or directory exists. FsExists = 0x4006, - /// Deletes a file or empty directory. FsDelete = 0x4007, - - // --- Log --- - /// Writes a generic string to the system log. LogWrite = 0x5001, - /// Writes a string to the system log with a specific numerical tag. LogWriteTag = 0x5002, - - // --- Asset (DMA) --- - /// Starts an asynchronous load of a file into a memory bank. AssetLoad = 0x6001, - /// Returns the status of a pending asset load (0=Loading, 1=Ready, 2=Error). AssetStatus = 0x6002, - /// Finalizes the asset loading, making it available for GFX/Audio. AssetCommit = 0x6003, - /// Cancels a pending asset load. AssetCancel = 0x6004, - - // --- Bank (Memory) --- - /// Returns information about a specific Memory Bank. BankInfo = 0x6101, - /// Returns information about a slot within a Memory Bank. 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, - /// Canonical module name for this syscall (e.g., "gfx", "input"). pub module: &'static str, - /// Canonical syscall name (snake_case, without module prefix). pub name: &'static str, - /// Canonical ABI version for this syscall identity. pub version: u16, - /// 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, - /// Whether this syscall may allocate VM heap objects. pub may_allocate: bool, - /// A coarse execution cost hint used by verifiers/schedulers. pub cost_hint: u32, } /// 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; - pub const ALL: CapFlags = SYSTEM | GFX | INPUT | AUDIO | FS | LOG | ASSET | BANK; -} - /// 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, } @@ -199,1365 +115,6 @@ pub struct SyscallRegistryEntry { pub meta: SyscallMeta, } -/// Canonical identity triple for a syscall. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SyscallIdentity { - pub module: &'static str, - pub name: &'static str, - pub version: u16, -} - -impl SyscallIdentity { - pub fn key(&self) -> (&'static str, &'static str, u16) { - (self.module, self.name, self.version) - } -} - -/// Resolved syscall information provided to the loader/VM. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SyscallResolved { - /// Numeric syscall id used at runtime (VM executes `SYSCALL ` only). - pub id: u32, - /// Associated metadata for verification/runtime checks. - pub meta: SyscallMeta, -} - -/// Load-time error for syscall resolution. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LoadError { - /// The (module, name, version) triple is not known by the host. - UnknownSyscall { module: &'static str, name: &'static str, version: u16 }, - /// The cartridge lacks required capabilities for the syscall. - MissingCapability { - required: CapFlags, - provided: CapFlags, - module: &'static str, - name: &'static str, - version: u16, - }, -} - -/// Load-time error for PBX-declared syscall resolution. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DeclaredLoadError { - /// The `(module, name, version)` triple is not known by the host. - UnknownSyscall { module: String, name: String, version: u16 }, - /// The cartridge lacks required capabilities for the syscall. - MissingCapability { - required: CapFlags, - provided: CapFlags, - module: String, - name: String, - version: u16, - }, - /// The PBX-declared ABI does not match the authoritative host metadata. - AbiMismatch { - module: String, - name: String, - version: u16, - declared_arg_slots: u16, - declared_ret_slots: u16, - expected_arg_slots: u16, - expected_ret_slots: u16, - }, -} - -fn resolve_syscall_impl(module: &str, name: &str, version: u16) -> Option { - for entry in SYSCALL_TABLE { - if entry.meta.module == module && entry.meta.name == name && entry.meta.version == version { - return Some(SyscallResolved { id: entry.meta.id, meta: entry.meta }); - } - } - None -} - -/// Resolve a canonical syscall identity to its numeric id and metadata. -/// -/// Returns `None` if the identity is unknown. -pub fn resolve_syscall( - module: &'static str, - name: &'static str, - version: u16, -) -> Option { - resolve_syscall_impl(module, name, version) -} - -/// Resolve all declared program syscalls and enforce capability gating. -/// -/// - `declared`: list of canonical identities required by the program. -/// - `caps`: capabilities granted to the cartridge/program. -/// -/// Fails deterministically if any syscall is unknown or requires missing caps. -pub fn resolve_program_syscalls( - declared: &[SyscallIdentity], - caps: CapFlags, -) -> Result, LoadError> { - let mut out = Vec::with_capacity(declared.len()); - for ident in declared { - let Some(res) = resolve_syscall(ident.module, ident.name, ident.version) else { - return Err(LoadError::UnknownSyscall { - module: ident.module, - name: ident.name, - version: ident.version, - }); - }; - // Capability gating: required must be subset of provided - let missing = res.meta.caps & !caps; - if missing != 0 { - return Err(LoadError::MissingCapability { - required: res.meta.caps, - provided: caps, - module: ident.module, - name: ident.name, - version: ident.version, - }); - } - out.push(res); - } - Ok(out) -} - -/// Resolve and validate syscall declarations carried by a PBX module. -/// -/// This is the loader-facing path for `BytecodeModule.syscalls`: -/// - canonical identity must exist in the host table -/// - capability requirements must be satisfied by granted cart flags -/// - declared ABI must match authoritative host metadata -pub fn resolve_declared_program_syscalls( - declared: &[prometeu_bytecode::SyscallDecl], - caps: CapFlags, -) -> Result, DeclaredLoadError> { - let mut out = Vec::with_capacity(declared.len()); - - for decl in declared { - let Some(res) = resolve_syscall_impl(&decl.module, &decl.name, decl.version) else { - return Err(DeclaredLoadError::UnknownSyscall { - module: decl.module.clone(), - name: decl.name.clone(), - version: decl.version, - }); - }; - - let missing = res.meta.caps & !caps; - if missing != 0 { - return Err(DeclaredLoadError::MissingCapability { - required: res.meta.caps, - provided: caps, - module: decl.module.clone(), - name: decl.name.clone(), - version: decl.version, - }); - } - - let expected_arg_slots = u16::from(res.meta.arg_slots); - let expected_ret_slots = res.meta.ret_slots; - if decl.arg_slots != expected_arg_slots || decl.ret_slots != expected_ret_slots { - return Err(DeclaredLoadError::AbiMismatch { - module: decl.module.clone(), - name: decl.name.clone(), - version: decl.version, - declared_arg_slots: decl.arg_slots, - declared_ret_slots: decl.ret_slots, - expected_arg_slots, - expected_ret_slots, - }); - } - - out.push(res); - } - - Ok(out) -} - -/// 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, - module: "system", - name: "has_cart", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::SYSTEM, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::SystemRunCart, - meta: SyscallMeta { - id: 0x0002, - module: "system", - name: "run_cart", - version: 1, - arg_slots: 0, - ret_slots: 0, - caps: caps::SYSTEM, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 50, - }, - }, - // --- GFX --- - SyscallRegistryEntry { - syscall: Syscall::GfxClear, - meta: SyscallMeta { - id: 0x1001, - module: "gfx", - name: "clear", - version: 1, - arg_slots: 1, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxFillRect, - meta: SyscallMeta { - id: 0x1002, - module: "gfx", - name: "fill_rect", - version: 1, - arg_slots: 5, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxDrawLine, - meta: SyscallMeta { - id: 0x1003, - module: "gfx", - name: "draw_line", - version: 1, - arg_slots: 5, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxDrawCircle, - meta: SyscallMeta { - id: 0x1004, - module: "gfx", - name: "draw_circle", - version: 1, - arg_slots: 4, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxDrawDisc, - meta: SyscallMeta { - id: 0x1005, - module: "gfx", - name: "draw_disc", - version: 1, - arg_slots: 5, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxDrawSquare, - meta: SyscallMeta { - id: 0x1006, - module: "gfx", - name: "draw_square", - version: 1, - arg_slots: 6, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxSetSprite, - meta: SyscallMeta { - id: 0x1007, - module: "gfx", - name: "set_sprite", - version: 1, - arg_slots: 10, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxDrawText, - meta: SyscallMeta { - id: 0x1008, - module: "gfx", - name: "draw_text", - version: 1, - arg_slots: 4, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::GfxClear565, - meta: SyscallMeta { - id: 0x1010, - module: "gfx", - name: "clear_565", - version: 1, - arg_slots: 1, - ret_slots: 0, - caps: caps::GFX, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - // --- Input --- - SyscallRegistryEntry { - syscall: Syscall::InputGetPad, - meta: SyscallMeta { - id: 0x2001, - module: "input", - name: "get_pad", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::InputGetPadPressed, - meta: SyscallMeta { - id: 0x2002, - module: "input", - name: "get_pad_pressed", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::InputGetPadReleased, - meta: SyscallMeta { - id: 0x2003, - module: "input", - name: "get_pad_released", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::InputGetPadHold, - meta: SyscallMeta { - id: 0x2004, - module: "input", - name: "get_pad_hold", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::InputPadSnapshot, - meta: SyscallMeta { - id: 0x2010, - module: "input", - name: "pad_snapshot", - version: 1, - arg_slots: 0, - ret_slots: 48, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::InputTouchSnapshot, - meta: SyscallMeta { - id: 0x2011, - module: "input", - name: "touch_snapshot", - version: 1, - arg_slots: 0, - ret_slots: 6, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchGetX, - meta: SyscallMeta { - id: 0x2101, - module: "input", - name: "touch_get_x", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchGetY, - meta: SyscallMeta { - id: 0x2102, - module: "input", - name: "touch_get_y", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchIsDown, - meta: SyscallMeta { - id: 0x2103, - module: "input", - name: "touch_is_down", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchIsPressed, - meta: SyscallMeta { - id: 0x2104, - module: "input", - name: "touch_is_pressed", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchIsReleased, - meta: SyscallMeta { - id: 0x2105, - module: "input", - name: "touch_is_released", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchGetHold, - meta: SyscallMeta { - id: 0x2106, - module: "input", - name: "touch_get_hold", - version: 1, - arg_slots: 0, - ret_slots: 1, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::TouchGetFinger, - meta: SyscallMeta { - id: 0x2107, - module: "input", - name: "touch_get_finger", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - // --- Input (Pad service-based) --- - SyscallRegistryEntry { - syscall: Syscall::PadGetUp, - meta: SyscallMeta { - id: 0x2200, - module: "input", - name: "pad_get_up", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetDown, - meta: SyscallMeta { - id: 0x2201, - module: "input", - name: "pad_get_down", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetLeft, - meta: SyscallMeta { - id: 0x2202, - module: "input", - name: "pad_get_left", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetRight, - meta: SyscallMeta { - id: 0x2203, - module: "input", - name: "pad_get_right", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetA, - meta: SyscallMeta { - id: 0x2204, - module: "input", - name: "pad_get_a", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetB, - meta: SyscallMeta { - id: 0x2205, - module: "input", - name: "pad_get_b", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetX, - meta: SyscallMeta { - id: 0x2206, - module: "input", - name: "pad_get_x", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetY, - meta: SyscallMeta { - id: 0x2207, - module: "input", - name: "pad_get_y", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetL, - meta: SyscallMeta { - id: 0x2208, - module: "input", - name: "pad_get_l", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetR, - meta: SyscallMeta { - id: 0x2209, - module: "input", - name: "pad_get_r", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetStart, - meta: SyscallMeta { - id: 0x220A, - module: "input", - name: "pad_get_start", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::PadGetSelect, - meta: SyscallMeta { - id: 0x220B, - module: "input", - name: "pad_get_select", - version: 1, - arg_slots: 0, - ret_slots: 4, - caps: caps::INPUT, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - // --- Audio --- - SyscallRegistryEntry { - syscall: Syscall::AudioPlaySample, - meta: SyscallMeta { - id: 0x3001, - module: "audio", - name: "play_sample", - version: 1, - arg_slots: 5, - ret_slots: 0, - caps: caps::AUDIO, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::AudioPlay, - meta: SyscallMeta { - id: 0x3002, - module: "audio", - name: "play", - version: 1, - arg_slots: 7, - ret_slots: 0, - caps: caps::AUDIO, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - // --- FS --- - SyscallRegistryEntry { - syscall: Syscall::FsOpen, - meta: SyscallMeta { - id: 0x4001, - module: "fs", - name: "open", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::FS, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::FsRead, - meta: SyscallMeta { - id: 0x4002, - module: "fs", - name: "read", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::FS, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::FsWrite, - meta: SyscallMeta { - id: 0x4003, - module: "fs", - name: "write", - version: 1, - arg_slots: 2, - ret_slots: 1, - caps: caps::FS, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::FsClose, - meta: SyscallMeta { - id: 0x4004, - module: "fs", - name: "close", - version: 1, - arg_slots: 1, - ret_slots: 0, - caps: caps::FS, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::FsListDir, - meta: SyscallMeta { - id: 0x4005, - module: "fs", - name: "list_dir", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::FS, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::FsExists, - meta: SyscallMeta { - id: 0x4006, - module: "fs", - name: "exists", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::FS, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::FsDelete, - meta: SyscallMeta { - id: 0x4007, - module: "fs", - name: "delete", - version: 1, - arg_slots: 1, - ret_slots: 0, - caps: caps::FS, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - // --- Log --- - SyscallRegistryEntry { - syscall: Syscall::LogWrite, - meta: SyscallMeta { - id: 0x5001, - module: "log", - name: "write", - version: 1, - arg_slots: 2, - ret_slots: 0, - caps: caps::LOG, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::LogWriteTag, - meta: SyscallMeta { - id: 0x5002, - module: "log", - name: "write_tag", - version: 1, - arg_slots: 3, - ret_slots: 0, - caps: caps::LOG, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 5, - }, - }, - // --- Asset/Bank --- - SyscallRegistryEntry { - syscall: Syscall::AssetLoad, - meta: SyscallMeta { - id: 0x6001, - module: "asset", - name: "load", - version: 1, - arg_slots: 3, - ret_slots: 1, - caps: caps::ASSET, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::AssetStatus, - meta: SyscallMeta { - id: 0x6002, - module: "asset", - name: "status", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::ASSET, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::AssetCommit, - meta: SyscallMeta { - id: 0x6003, - module: "asset", - name: "commit", - version: 1, - arg_slots: 1, - ret_slots: 0, - caps: caps::ASSET, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::AssetCancel, - meta: SyscallMeta { - id: 0x6004, - module: "asset", - name: "cancel", - version: 1, - arg_slots: 1, - ret_slots: 0, - caps: caps::ASSET, - determinism: Determinism::NonDeterministic, - may_allocate: false, - cost_hint: 20, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::BankInfo, - meta: SyscallMeta { - id: 0x6101, - module: "bank", - name: "info", - version: 1, - arg_slots: 1, - ret_slots: 1, - caps: caps::BANK, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, - SyscallRegistryEntry { - syscall: Syscall::BankSlotInfo, - meta: SyscallMeta { - id: 0x6102, - module: "bank", - name: "slot_info", - version: 1, - arg_slots: 2, - ret_slots: 1, - caps: caps::BANK, - determinism: Determinism::Deterministic, - may_allocate: false, - cost_hint: 1, - }, - }, -]; - -/// 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 { - match id { - 0x0001 => Some(Self::SystemHasCart), - 0x0002 => Some(Self::SystemRunCart), - 0x1001 => Some(Self::GfxClear), - 0x1002 => Some(Self::GfxFillRect), - 0x1003 => Some(Self::GfxDrawLine), - 0x1004 => Some(Self::GfxDrawCircle), - 0x1005 => Some(Self::GfxDrawDisc), - 0x1006 => Some(Self::GfxDrawSquare), - 0x1007 => Some(Self::GfxSetSprite), - 0x1008 => Some(Self::GfxDrawText), - 0x1010 => Some(Self::GfxClear565), - 0x2001 => Some(Self::InputGetPad), - 0x2002 => Some(Self::InputGetPadPressed), - 0x2003 => Some(Self::InputGetPadReleased), - 0x2004 => Some(Self::InputGetPadHold), - 0x2010 => Some(Self::InputPadSnapshot), - 0x2011 => Some(Self::InputTouchSnapshot), - 0x2101 => Some(Self::TouchGetX), - 0x2102 => Some(Self::TouchGetY), - 0x2103 => Some(Self::TouchIsDown), - 0x2104 => Some(Self::TouchIsPressed), - 0x2105 => Some(Self::TouchIsReleased), - 0x2106 => Some(Self::TouchGetHold), - 0x2107 => Some(Self::TouchGetFinger), - 0x2200 => Some(Self::PadGetUp), - 0x2201 => Some(Self::PadGetDown), - 0x2202 => Some(Self::PadGetLeft), - 0x2203 => Some(Self::PadGetRight), - 0x2204 => Some(Self::PadGetA), - 0x2205 => Some(Self::PadGetB), - 0x2206 => Some(Self::PadGetX), - 0x2207 => Some(Self::PadGetY), - 0x2208 => Some(Self::PadGetL), - 0x2209 => Some(Self::PadGetR), - 0x220A => Some(Self::PadGetStart), - 0x220B => Some(Self::PadGetSelect), - 0x3001 => Some(Self::AudioPlaySample), - 0x3002 => Some(Self::AudioPlay), - 0x4001 => Some(Self::FsOpen), - 0x4002 => Some(Self::FsRead), - 0x4003 => Some(Self::FsWrite), - 0x4004 => Some(Self::FsClose), - 0x4005 => Some(Self::FsListDir), - 0x4006 => Some(Self::FsExists), - 0x4007 => Some(Self::FsDelete), - 0x5001 => Some(Self::LogWrite), - 0x5002 => Some(Self::LogWriteTag), - 0x6001 => Some(Self::AssetLoad), - 0x6002 => Some(Self::AssetStatus), - 0x6003 => Some(Self::AssetCommit), - 0x6004 => Some(Self::AssetCancel), - 0x6101 => Some(Self::BankInfo), - 0x6102 => Some(Self::BankSlotInfo), - _ => None, - } - } - - pub fn args_count(&self) -> usize { - meta_for(*self).arg_slots as usize - } - - pub fn results_count(&self) -> usize { - meta_for(*self).ret_slots as usize - } - - pub fn name(&self) -> &'static str { - match self { - Self::SystemHasCart => "SystemHasCart", - Self::SystemRunCart => "SystemRunCart", - Self::GfxClear => "GfxClear", - Self::GfxFillRect => "GfxFillRect", - Self::GfxDrawLine => "GfxDrawLine", - Self::GfxDrawCircle => "GfxDrawCircle", - Self::GfxDrawDisc => "GfxDrawDisc", - Self::GfxDrawSquare => "GfxDrawSquare", - Self::GfxSetSprite => "GfxSetSprite", - Self::GfxDrawText => "GfxDrawText", - Self::GfxClear565 => "GfxClear565", - Self::InputGetPad => "InputGetPad", - Self::InputGetPadPressed => "InputGetPadPressed", - Self::InputGetPadReleased => "InputGetPadReleased", - Self::InputGetPadHold => "InputGetPadHold", - Self::InputPadSnapshot => "InputPadSnapshot", - Self::InputTouchSnapshot => "InputTouchSnapshot", - Self::TouchGetX => "TouchGetX", - Self::TouchGetY => "TouchGetY", - Self::TouchIsDown => "TouchIsDown", - Self::TouchIsPressed => "TouchIsPressed", - Self::TouchIsReleased => "TouchIsReleased", - Self::TouchGetHold => "TouchGetHold", - Self::TouchGetFinger => "TouchGetFinger", - Self::PadGetUp => "PadGetUp", - Self::PadGetDown => "PadGetDown", - Self::PadGetLeft => "PadGetLeft", - Self::PadGetRight => "PadGetRight", - Self::PadGetA => "PadGetA", - Self::PadGetB => "PadGetB", - Self::PadGetX => "PadGetX", - Self::PadGetY => "PadGetY", - Self::PadGetL => "PadGetL", - Self::PadGetR => "PadGetR", - Self::PadGetStart => "PadGetStart", - Self::PadGetSelect => "PadGetSelect", - Self::AudioPlaySample => "AudioPlaySample", - Self::AudioPlay => "AudioPlay", - Self::FsOpen => "FsOpen", - Self::FsRead => "FsRead", - Self::FsWrite => "FsWrite", - Self::FsClose => "FsClose", - Self::FsListDir => "FsListDir", - Self::FsExists => "FsExists", - Self::FsDelete => "FsDelete", - Self::LogWrite => "LogWrite", - Self::LogWriteTag => "LogWriteTag", - Self::AssetLoad => "AssetLoad", - Self::AssetStatus => "AssetStatus", - Self::AssetCommit => "AssetCommit", - Self::AssetCancel => "AssetCancel", - Self::BankInfo => "BankInfo", - Self::BankSlotInfo => "BankSlotInfo", - } - } -} - -#[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); - // identity fields must be present - assert!(!m.module.is_empty(), "module must be non-empty for id=0x{:08X}", m.id); - assert!(!m.name.is_empty(), "name must be non-empty for id=0x{:08X}", m.id); - assert!(m.version > 0, "version must be > 0 for id=0x{:08X}", m.id); - } - - // 2) Table must not contain duplicates and must map back to a valid enum. - use std::collections::HashSet; - let mut ids = HashSet::new(); - let mut identities = 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); - - // (module,name,version) must be unique - let key = (e.meta.module, e.meta.name, e.meta.version); - assert!( - identities.insert(key), - "duplicate canonical identity: ({}.{}, v{})", - e.meta.module, - e.meta.name, - e.meta.version - ); - } - - // 3) Table and explicit list sizes must match (guard against omissions). - assert_eq!(SYSCALL_TABLE.len(), all_syscalls().len()); - } - - #[test] - fn resolver_returns_expected_id_for_known_identity() { - // Pick a stable entry from the table - let id = resolve_syscall("gfx", "clear", 1).expect("known identity must resolve"); - assert_eq!(id.id, 0x1001); - assert_eq!(id.meta.module, "gfx"); - assert_eq!(id.meta.name, "clear"); - assert_eq!(id.meta.version, 1); - } - - #[test] - fn resolver_rejects_unknown_identity() { - let res = resolve_syscall("gfx", "nonexistent", 1); - assert!(res.is_none()); - - // And via the program-level resolver - let requested = [SyscallIdentity { module: "gfx", name: "nonexistent", version: 1 }]; - let err = resolve_program_syscalls(&requested, 0).unwrap_err(); - match err { - LoadError::UnknownSyscall { module, name, version } => { - assert_eq!(module, "gfx"); - assert_eq!(name, "nonexistent"); - assert_eq!(version, 1); - } - _ => panic!("expected UnknownSyscall error"), - } - } - - #[test] - fn resolver_enforces_capabilities() { - // Choose a syscall that requires GFX caps. - let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }]; - // Provide no caps → should fail. - let err = resolve_program_syscalls(&requested, 0).unwrap_err(); - match err { - LoadError::MissingCapability { required, provided, module, name, version } => { - assert_eq!(module, "gfx"); - assert_eq!(name, "clear"); - assert_eq!(version, 1); - assert_ne!(required, 0); - assert_eq!(provided, 0); - } - _ => panic!("expected MissingCapability error"), - } - - // Provide correct caps → should pass. - let ok = resolve_program_syscalls(&requested, caps::GFX).expect("must resolve with caps"); - assert_eq!(ok.len(), 1); - assert_eq!(ok[0].id, 0x1001); - } - - #[test] - fn declared_resolver_returns_expected_id_for_known_identity() { - let declared = [prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }]; - - let ok = - resolve_declared_program_syscalls(&declared, caps::GFX).expect("must resolve with ABI"); - assert_eq!(ok.len(), 1); - assert_eq!(ok[0].id, 0x1001); - } - - #[test] - fn declared_resolver_rejects_unknown_identity() { - let declared = [prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "nonexistent".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }]; - - let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err(); - assert_eq!( - err, - DeclaredLoadError::UnknownSyscall { - module: "gfx".into(), - name: "nonexistent".into(), - version: 1, - } - ); - } - - #[test] - fn declared_resolver_rejects_missing_capability() { - let declared = [prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }]; - - let err = resolve_declared_program_syscalls(&declared, caps::NONE).unwrap_err(); - assert_eq!( - err, - DeclaredLoadError::MissingCapability { - required: caps::GFX, - provided: caps::NONE, - module: "gfx".into(), - name: "clear".into(), - version: 1, - } - ); - } - - #[test] - fn declared_resolver_rejects_abi_mismatch() { - let declared = [prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "draw_line".into(), - version: 1, - arg_slots: 4, - ret_slots: 0, - }]; - - let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err(); - assert_eq!( - err, - DeclaredLoadError::AbiMismatch { - module: "gfx".into(), - name: "draw_line".into(), - version: 1, - declared_arg_slots: 4, - declared_ret_slots: 0, - expected_arg_slots: 5, - expected_ret_slots: 0, - } - ); - } + registry::meta_for(syscall) } diff --git a/crates/console/prometeu-hal/src/syscalls/caps.rs b/crates/console/prometeu-hal/src/syscalls/caps.rs new file mode 100644 index 00000000..109f25ce --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/caps.rs @@ -0,0 +1,12 @@ +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; +pub const ALL: CapFlags = SYSTEM | GFX | INPUT | AUDIO | FS | LOG | ASSET | BANK; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/asset.rs b/crates/console/prometeu-hal/src/syscalls/domains/asset.rs new file mode 100644 index 00000000..661c1798 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/asset.rs @@ -0,0 +1,53 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::AssetLoad, + "asset", + "load", + 1, + 3, + 1, + caps::ASSET, + Determinism::NonDeterministic, + false, + 20, + ), + entry( + Syscall::AssetStatus, + "asset", + "status", + 1, + 1, + 1, + caps::ASSET, + Determinism::NonDeterministic, + false, + 1, + ), + entry( + Syscall::AssetCommit, + "asset", + "commit", + 1, + 1, + 0, + caps::ASSET, + Determinism::NonDeterministic, + false, + 20, + ), + entry( + Syscall::AssetCancel, + "asset", + "cancel", + 1, + 1, + 0, + caps::ASSET, + Determinism::NonDeterministic, + false, + 20, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/audio.rs b/crates/console/prometeu-hal/src/syscalls/domains/audio.rs new file mode 100644 index 00000000..ca1dc7ce --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/audio.rs @@ -0,0 +1,29 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::AudioPlaySample, + "audio", + "play_sample", + 1, + 5, + 0, + caps::AUDIO, + Determinism::Deterministic, + false, + 5, + ), + entry( + Syscall::AudioPlay, + "audio", + "play", + 1, + 7, + 0, + caps::AUDIO, + Determinism::Deterministic, + false, + 5, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/bank.rs b/crates/console/prometeu-hal/src/syscalls/domains/bank.rs new file mode 100644 index 00000000..b8665707 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/bank.rs @@ -0,0 +1,29 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::BankInfo, + "bank", + "info", + 1, + 1, + 1, + caps::BANK, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::BankSlotInfo, + "bank", + "slot_info", + 1, + 2, + 1, + caps::BANK, + Determinism::Deterministic, + false, + 1, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/fs.rs b/crates/console/prometeu-hal/src/syscalls/domains/fs.rs new file mode 100644 index 00000000..cd060cbf --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/fs.rs @@ -0,0 +1,78 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::FsOpen, + "fs", + "open", + 1, + 1, + 1, + caps::FS, + Determinism::NonDeterministic, + false, + 20, + ), + entry( + Syscall::FsRead, + "fs", + "read", + 1, + 1, + 1, + caps::FS, + Determinism::NonDeterministic, + false, + 20, + ), + entry( + Syscall::FsWrite, + "fs", + "write", + 1, + 2, + 1, + caps::FS, + Determinism::NonDeterministic, + false, + 20, + ), + entry(Syscall::FsClose, "fs", "close", 1, 1, 0, caps::FS, Determinism::Deterministic, false, 5), + entry( + Syscall::FsListDir, + "fs", + "list_dir", + 1, + 1, + 1, + caps::FS, + Determinism::NonDeterministic, + false, + 20, + ), + entry( + Syscall::FsExists, + "fs", + "exists", + 1, + 1, + 1, + caps::FS, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::FsDelete, + "fs", + "delete", + 1, + 1, + 0, + caps::FS, + Determinism::NonDeterministic, + false, + 20, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs b/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs new file mode 100644 index 00000000..280cdff4 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs @@ -0,0 +1,113 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::GfxClear, + "gfx", + "clear", + 1, + 1, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 20, + ), + entry( + Syscall::GfxFillRect, + "gfx", + "fill_rect", + 1, + 5, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 20, + ), + entry( + Syscall::GfxDrawLine, + "gfx", + "draw_line", + 1, + 5, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 5, + ), + entry( + Syscall::GfxDrawCircle, + "gfx", + "draw_circle", + 1, + 4, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 5, + ), + entry( + Syscall::GfxDrawDisc, + "gfx", + "draw_disc", + 1, + 5, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 5, + ), + entry( + Syscall::GfxDrawSquare, + "gfx", + "draw_square", + 1, + 6, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 5, + ), + entry( + Syscall::GfxSetSprite, + "gfx", + "set_sprite", + 1, + 10, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 5, + ), + entry( + Syscall::GfxDrawText, + "gfx", + "draw_text", + 1, + 4, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 20, + ), + entry( + Syscall::GfxClear565, + "gfx", + "clear_565", + 1, + 1, + 0, + caps::GFX, + Determinism::Deterministic, + false, + 20, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/input.rs b/crates/console/prometeu-hal/src/syscalls/domains/input.rs new file mode 100644 index 00000000..acbb591a --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/input.rs @@ -0,0 +1,305 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::InputGetPad, + "input", + "get_pad", + 1, + 1, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::InputGetPadPressed, + "input", + "get_pad_pressed", + 1, + 1, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::InputGetPadReleased, + "input", + "get_pad_released", + 1, + 1, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::InputGetPadHold, + "input", + "get_pad_hold", + 1, + 1, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::InputPadSnapshot, + "input", + "pad_snapshot", + 1, + 0, + 48, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::InputTouchSnapshot, + "input", + "touch_snapshot", + 1, + 0, + 6, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchGetX, + "input", + "touch_get_x", + 1, + 0, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchGetY, + "input", + "touch_get_y", + 1, + 0, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchIsDown, + "input", + "touch_is_down", + 1, + 0, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchIsPressed, + "input", + "touch_is_pressed", + 1, + 0, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchIsReleased, + "input", + "touch_is_released", + 1, + 0, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchGetHold, + "input", + "touch_get_hold", + 1, + 0, + 1, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::TouchGetFinger, + "input", + "touch_get_finger", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetUp, + "input", + "pad_get_up", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetDown, + "input", + "pad_get_down", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetLeft, + "input", + "pad_get_left", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetRight, + "input", + "pad_get_right", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetA, + "input", + "pad_get_a", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetB, + "input", + "pad_get_b", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetX, + "input", + "pad_get_x", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetY, + "input", + "pad_get_y", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetL, + "input", + "pad_get_l", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetR, + "input", + "pad_get_r", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetStart, + "input", + "pad_get_start", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::PadGetSelect, + "input", + "pad_get_select", + 1, + 0, + 4, + caps::INPUT, + Determinism::Deterministic, + false, + 1, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/log.rs b/crates/console/prometeu-hal/src/syscalls/domains/log.rs new file mode 100644 index 00000000..8b03af1f --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/log.rs @@ -0,0 +1,29 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::LogWrite, + "log", + "write", + 1, + 2, + 0, + caps::LOG, + Determinism::NonDeterministic, + false, + 5, + ), + entry( + Syscall::LogWriteTag, + "log", + "write_tag", + 1, + 3, + 0, + caps::LOG, + Determinism::NonDeterministic, + false, + 5, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/mod.rs b/crates/console/prometeu-hal/src/syscalls/domains/mod.rs new file mode 100644 index 00000000..07f6abcf --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/mod.rs @@ -0,0 +1,51 @@ +mod asset; +mod audio; +mod bank; +mod fs; +mod gfx; +mod input; +mod log; +mod system; + +use super::{CapFlags, Determinism, Syscall, SyscallMeta, SyscallRegistryEntry}; + +pub(crate) const fn entry( + syscall: Syscall, + module: &'static str, + name: &'static str, + version: u16, + arg_slots: u8, + ret_slots: u16, + caps: CapFlags, + determinism: Determinism, + may_allocate: bool, + cost_hint: u32, +) -> SyscallRegistryEntry { + SyscallRegistryEntry { + syscall, + meta: SyscallMeta { + id: syscall as u32, + module, + name, + version, + arg_slots, + ret_slots, + caps, + determinism, + may_allocate, + cost_hint, + }, + } +} + +pub(crate) fn all_entries() -> impl Iterator { + system::ENTRIES + .iter() + .chain(gfx::ENTRIES.iter()) + .chain(input::ENTRIES.iter()) + .chain(audio::ENTRIES.iter()) + .chain(fs::ENTRIES.iter()) + .chain(log::ENTRIES.iter()) + .chain(asset::ENTRIES.iter()) + .chain(bank::ENTRIES.iter()) +} diff --git a/crates/console/prometeu-hal/src/syscalls/domains/system.rs b/crates/console/prometeu-hal/src/syscalls/domains/system.rs new file mode 100644 index 00000000..2f5259a7 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/system.rs @@ -0,0 +1,29 @@ +use super::entry; +use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + entry( + Syscall::SystemHasCart, + "system", + "has_cart", + 1, + 0, + 1, + caps::SYSTEM, + Determinism::Deterministic, + false, + 1, + ), + entry( + Syscall::SystemRunCart, + "system", + "run_cart", + 1, + 0, + 0, + caps::SYSTEM, + Determinism::NonDeterministic, + false, + 50, + ), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/registry.rs b/crates/console/prometeu-hal/src/syscalls/registry.rs new file mode 100644 index 00000000..1e8e9d0e --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/registry.rs @@ -0,0 +1,137 @@ +use super::{Syscall, SyscallMeta, domains}; + +pub(crate) fn meta_for(syscall: Syscall) -> &'static SyscallMeta { + for entry in domains::all_entries() { + if entry.syscall == syscall { + return &entry.meta; + } + } + panic!("Missing SyscallMeta for {:?}", syscall); +} + +impl Syscall { + pub fn from_u32(id: u32) -> Option { + match id { + 0x0001 => Some(Self::SystemHasCart), + 0x0002 => Some(Self::SystemRunCart), + 0x1001 => Some(Self::GfxClear), + 0x1002 => Some(Self::GfxFillRect), + 0x1003 => Some(Self::GfxDrawLine), + 0x1004 => Some(Self::GfxDrawCircle), + 0x1005 => Some(Self::GfxDrawDisc), + 0x1006 => Some(Self::GfxDrawSquare), + 0x1007 => Some(Self::GfxSetSprite), + 0x1008 => Some(Self::GfxDrawText), + 0x1010 => Some(Self::GfxClear565), + 0x2001 => Some(Self::InputGetPad), + 0x2002 => Some(Self::InputGetPadPressed), + 0x2003 => Some(Self::InputGetPadReleased), + 0x2004 => Some(Self::InputGetPadHold), + 0x2010 => Some(Self::InputPadSnapshot), + 0x2011 => Some(Self::InputTouchSnapshot), + 0x2101 => Some(Self::TouchGetX), + 0x2102 => Some(Self::TouchGetY), + 0x2103 => Some(Self::TouchIsDown), + 0x2104 => Some(Self::TouchIsPressed), + 0x2105 => Some(Self::TouchIsReleased), + 0x2106 => Some(Self::TouchGetHold), + 0x2107 => Some(Self::TouchGetFinger), + 0x2200 => Some(Self::PadGetUp), + 0x2201 => Some(Self::PadGetDown), + 0x2202 => Some(Self::PadGetLeft), + 0x2203 => Some(Self::PadGetRight), + 0x2204 => Some(Self::PadGetA), + 0x2205 => Some(Self::PadGetB), + 0x2206 => Some(Self::PadGetX), + 0x2207 => Some(Self::PadGetY), + 0x2208 => Some(Self::PadGetL), + 0x2209 => Some(Self::PadGetR), + 0x220A => Some(Self::PadGetStart), + 0x220B => Some(Self::PadGetSelect), + 0x3001 => Some(Self::AudioPlaySample), + 0x3002 => Some(Self::AudioPlay), + 0x4001 => Some(Self::FsOpen), + 0x4002 => Some(Self::FsRead), + 0x4003 => Some(Self::FsWrite), + 0x4004 => Some(Self::FsClose), + 0x4005 => Some(Self::FsListDir), + 0x4006 => Some(Self::FsExists), + 0x4007 => Some(Self::FsDelete), + 0x5001 => Some(Self::LogWrite), + 0x5002 => Some(Self::LogWriteTag), + 0x6001 => Some(Self::AssetLoad), + 0x6002 => Some(Self::AssetStatus), + 0x6003 => Some(Self::AssetCommit), + 0x6004 => Some(Self::AssetCancel), + 0x6101 => Some(Self::BankInfo), + 0x6102 => Some(Self::BankSlotInfo), + _ => None, + } + } + + pub fn args_count(&self) -> usize { + super::meta_for(*self).arg_slots as usize + } + + pub fn results_count(&self) -> usize { + super::meta_for(*self).ret_slots as usize + } + + pub fn name(&self) -> &'static str { + match self { + Self::SystemHasCart => "SystemHasCart", + Self::SystemRunCart => "SystemRunCart", + Self::GfxClear => "GfxClear", + Self::GfxFillRect => "GfxFillRect", + Self::GfxDrawLine => "GfxDrawLine", + Self::GfxDrawCircle => "GfxDrawCircle", + Self::GfxDrawDisc => "GfxDrawDisc", + Self::GfxDrawSquare => "GfxDrawSquare", + Self::GfxSetSprite => "GfxSetSprite", + Self::GfxDrawText => "GfxDrawText", + Self::GfxClear565 => "GfxClear565", + Self::InputGetPad => "InputGetPad", + Self::InputGetPadPressed => "InputGetPadPressed", + Self::InputGetPadReleased => "InputGetPadReleased", + Self::InputGetPadHold => "InputGetPadHold", + Self::InputPadSnapshot => "InputPadSnapshot", + Self::InputTouchSnapshot => "InputTouchSnapshot", + Self::TouchGetX => "TouchGetX", + Self::TouchGetY => "TouchGetY", + Self::TouchIsDown => "TouchIsDown", + Self::TouchIsPressed => "TouchIsPressed", + Self::TouchIsReleased => "TouchIsReleased", + Self::TouchGetHold => "TouchGetHold", + Self::TouchGetFinger => "TouchGetFinger", + Self::PadGetUp => "PadGetUp", + Self::PadGetDown => "PadGetDown", + Self::PadGetLeft => "PadGetLeft", + Self::PadGetRight => "PadGetRight", + Self::PadGetA => "PadGetA", + Self::PadGetB => "PadGetB", + Self::PadGetX => "PadGetX", + Self::PadGetY => "PadGetY", + Self::PadGetL => "PadGetL", + Self::PadGetR => "PadGetR", + Self::PadGetStart => "PadGetStart", + Self::PadGetSelect => "PadGetSelect", + Self::AudioPlaySample => "AudioPlaySample", + Self::AudioPlay => "AudioPlay", + Self::FsOpen => "FsOpen", + Self::FsRead => "FsRead", + Self::FsWrite => "FsWrite", + Self::FsClose => "FsClose", + Self::FsListDir => "FsListDir", + Self::FsExists => "FsExists", + Self::FsDelete => "FsDelete", + Self::LogWrite => "LogWrite", + Self::LogWriteTag => "LogWriteTag", + Self::AssetLoad => "AssetLoad", + Self::AssetStatus => "AssetStatus", + Self::AssetCommit => "AssetCommit", + Self::AssetCancel => "AssetCancel", + Self::BankInfo => "BankInfo", + Self::BankSlotInfo => "BankSlotInfo", + } + } +} diff --git a/crates/console/prometeu-hal/src/syscalls/resolver.rs b/crates/console/prometeu-hal/src/syscalls/resolver.rs new file mode 100644 index 00000000..2f774e06 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/resolver.rs @@ -0,0 +1,156 @@ +use super::{CapFlags, SyscallMeta, domains}; + +/// Canonical identity triple for a syscall. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SyscallIdentity { + pub module: &'static str, + pub name: &'static str, + pub version: u16, +} + +impl SyscallIdentity { + pub fn key(&self) -> (&'static str, &'static str, u16) { + (self.module, self.name, self.version) + } +} + +/// Resolved syscall information provided to the loader/VM. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SyscallResolved { + pub id: u32, + pub meta: SyscallMeta, +} + +/// Load-time error for syscall resolution. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoadError { + UnknownSyscall { + module: &'static str, + name: &'static str, + version: u16, + }, + MissingCapability { + required: CapFlags, + provided: CapFlags, + module: &'static str, + name: &'static str, + version: u16, + }, +} + +/// Load-time error for PBX-declared syscall resolution. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeclaredLoadError { + UnknownSyscall { + module: String, + name: String, + version: u16, + }, + MissingCapability { + required: CapFlags, + provided: CapFlags, + module: String, + name: String, + version: u16, + }, + AbiMismatch { + module: String, + name: String, + version: u16, + declared_arg_slots: u16, + declared_ret_slots: u16, + expected_arg_slots: u16, + expected_ret_slots: u16, + }, +} + +fn resolve_syscall_impl(module: &str, name: &str, version: u16) -> Option { + for entry in domains::all_entries() { + if entry.meta.module == module && entry.meta.name == name && entry.meta.version == version { + return Some(SyscallResolved { id: entry.meta.id, meta: entry.meta }); + } + } + None +} + +pub fn resolve_syscall( + module: &'static str, + name: &'static str, + version: u16, +) -> Option { + resolve_syscall_impl(module, name, version) +} + +pub fn resolve_program_syscalls( + declared: &[SyscallIdentity], + caps: CapFlags, +) -> Result, LoadError> { + let mut out = Vec::with_capacity(declared.len()); + for ident in declared { + let Some(res) = resolve_syscall(ident.module, ident.name, ident.version) else { + return Err(LoadError::UnknownSyscall { + module: ident.module, + name: ident.name, + version: ident.version, + }); + }; + let missing = res.meta.caps & !caps; + if missing != 0 { + return Err(LoadError::MissingCapability { + required: res.meta.caps, + provided: caps, + module: ident.module, + name: ident.name, + version: ident.version, + }); + } + out.push(res); + } + Ok(out) +} + +pub fn resolve_declared_program_syscalls( + declared: &[prometeu_bytecode::SyscallDecl], + caps: CapFlags, +) -> Result, DeclaredLoadError> { + let mut out = Vec::with_capacity(declared.len()); + + for decl in declared { + let Some(res) = resolve_syscall_impl(&decl.module, &decl.name, decl.version) else { + return Err(DeclaredLoadError::UnknownSyscall { + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + }); + }; + + let missing = res.meta.caps & !caps; + if missing != 0 { + return Err(DeclaredLoadError::MissingCapability { + required: res.meta.caps, + provided: caps, + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + }); + } + + let expected_arg_slots = u16::from(res.meta.arg_slots); + let expected_ret_slots = res.meta.ret_slots; + if decl.arg_slots != expected_arg_slots || decl.ret_slots != expected_ret_slots { + return Err(DeclaredLoadError::AbiMismatch { + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + declared_arg_slots: decl.arg_slots, + declared_ret_slots: decl.ret_slots, + expected_arg_slots, + expected_ret_slots, + }); + } + + out.push(res); + } + + Ok(out) +} diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs new file mode 100644 index 00000000..ade9cda7 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -0,0 +1,169 @@ +use super::*; + +fn all_syscalls() -> Vec { + domains::all_entries().map(|entry| entry.syscall).collect() +} + +#[test] +fn every_syscall_has_metadata() { + for sc in all_syscalls() { + let m = meta_for(sc); + assert_eq!(m.id, sc as u32, "id mismatch for {:?}", sc); + assert!(!m.module.is_empty(), "module must be non-empty for id=0x{:08X}", m.id); + assert!(!m.name.is_empty(), "name must be non-empty for id=0x{:08X}", m.id); + assert!(m.version > 0, "version must be > 0 for id=0x{:08X}", m.id); + } + + use std::collections::HashSet; + let mut ids = HashSet::new(); + let mut identities = HashSet::new(); + let mut count = 0usize; + for entry in domains::all_entries() { + count += 1; + assert!(ids.insert(entry.meta.id), "duplicate syscall id 0x{:08X}", entry.meta.id); + let parsed = Syscall::from_u32(entry.meta.id).expect("id not recognized by enum mapping"); + assert_eq!(parsed as u32, entry.meta.id); + + let key = (entry.meta.module, entry.meta.name, entry.meta.version); + assert!( + identities.insert(key), + "duplicate canonical identity: ({}.{}, v{})", + entry.meta.module, + entry.meta.name, + entry.meta.version + ); + } + + assert_eq!(count, all_syscalls().len()); +} + +#[test] +fn resolver_returns_expected_id_for_known_identity() { + let id = resolve_syscall("gfx", "clear", 1).expect("known identity must resolve"); + assert_eq!(id.id, 0x1001); + assert_eq!(id.meta.module, "gfx"); + assert_eq!(id.meta.name, "clear"); + assert_eq!(id.meta.version, 1); +} + +#[test] +fn resolver_rejects_unknown_identity() { + let res = resolve_syscall("gfx", "nonexistent", 1); + assert!(res.is_none()); + + let requested = [SyscallIdentity { module: "gfx", name: "nonexistent", version: 1 }]; + let err = resolve_program_syscalls(&requested, 0).unwrap_err(); + match err { + LoadError::UnknownSyscall { module, name, version } => { + assert_eq!(module, "gfx"); + assert_eq!(name, "nonexistent"); + assert_eq!(version, 1); + } + _ => panic!("expected UnknownSyscall error"), + } +} + +#[test] +fn resolver_enforces_capabilities() { + let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }]; + let err = resolve_program_syscalls(&requested, 0).unwrap_err(); + match err { + LoadError::MissingCapability { required, provided, module, name, version } => { + assert_eq!(module, "gfx"); + assert_eq!(name, "clear"); + assert_eq!(version, 1); + assert_ne!(required, 0); + assert_eq!(provided, 0); + } + _ => panic!("expected MissingCapability error"), + } + + let ok = resolve_program_syscalls(&requested, caps::GFX).expect("must resolve with caps"); + assert_eq!(ok.len(), 1); + assert_eq!(ok[0].id, 0x1001); +} + +#[test] +fn declared_resolver_returns_expected_id_for_known_identity() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }]; + + let ok = + resolve_declared_program_syscalls(&declared, caps::GFX).expect("must resolve with ABI"); + assert_eq!(ok.len(), 1); + assert_eq!(ok[0].id, 0x1001); +} + +#[test] +fn declared_resolver_rejects_unknown_identity() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "nonexistent".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }]; + + let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::UnknownSyscall { + module: "gfx".into(), + name: "nonexistent".into(), + version: 1, + } + ); +} + +#[test] +fn declared_resolver_rejects_missing_capability() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }]; + + let err = resolve_declared_program_syscalls(&declared, caps::NONE).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::MissingCapability { + required: caps::GFX, + provided: caps::NONE, + module: "gfx".into(), + name: "clear".into(), + version: 1, + } + ); +} + +#[test] +fn declared_resolver_rejects_abi_mismatch() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "draw_line".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }]; + + let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::AbiMismatch { + module: "gfx".into(), + name: "draw_line".into(), + version: 1, + declared_arg_slots: 4, + declared_ret_slots: 0, + expected_arg_slots: 5, + expected_ret_slots: 0, + } + ); +} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime.rs b/crates/console/prometeu-system/src/virtual_machine_runtime.rs index 11df8788..02f6b9c2 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime.rs @@ -1,1479 +1,48 @@ +mod dispatch; +mod lifecycle; +#[cfg(test)] +mod tests; +mod tick; + use crate::CrashReport; -use crate::fs::{FsBackend, FsState, VirtualFS}; -use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; -use prometeu_hal::asset::{BankType, LoadStatus, SlotRef}; -use prometeu_hal::button::Button; -use prometeu_hal::cartridge::{AppMode, Cartridge}; -use prometeu_hal::color::Color; -use prometeu_hal::log::{LogLevel, LogService, LogSource}; -use prometeu_hal::sprite::Sprite; -use prometeu_hal::syscalls::Syscall; +use crate::fs::{FsState, VirtualFS}; +use prometeu_hal::cartridge::AppMode; +use prometeu_hal::log::LogService; use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame}; -use prometeu_hal::tile::Tile; -use prometeu_hal::vm_fault::VmFault; -use prometeu_hal::{HardwareBridge, InputSignals}; -use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; -use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine}; +use prometeu_vm::VirtualMachine; use std::collections::HashMap; use std::time::Instant; pub struct VirtualMachineRuntime { - /// Host Tick Index: Incremented on every host hardware update (usually 60Hz). pub tick_index: u64, - /// Logical Frame Index: Incremented only when an app successfully completes a full frame via `FRAME_SYNC`. pub logical_frame_index: u64, - /// Execution State: True if the VM is currently mid-frame. pub logical_frame_active: bool, - /// Cycle Budget: The number of PVM cycles remaining for the current logical frame. pub logical_frame_remaining_cycles: u64, - /// Performance Metric: Time spent by the host CPU processing the last tick (in microseconds). pub last_frame_cpu_time_us: u64, - - // --- Filesystem --- - /// The virtual filesystem interface, abstracting the physical storage. pub fs: VirtualFS, - /// Current health and connection status of the virtual filesystem. pub fs_state: FsState, - /// Active file handles mapping IDs to their virtual paths. pub open_files: HashMap, - /// Generator for unique, non-zero file handles. pub next_handle: u32, - - // --- Logging & Identity --- - /// The centralized service for recording and retrieving system logs. pub log_service: LogService, - /// Metadata for the currently running application. pub current_app_id: u32, pub current_cartridge_title: String, pub current_cartridge_app_version: String, pub current_cartridge_app_mode: AppMode, pub current_entrypoint: String, - /// Rate-limiter to prevent apps from flooding the log buffer and killing performance. pub logs_written_this_frame: HashMap, - - // --- Monitoring & Debugging --- - /// Running counters for the current execution slice. pub telemetry_current: TelemetryFrame, - /// The results of the last successfully completed logical frame. pub telemetry_last: TelemetryFrame, - /// Last terminal crash report surfaced by the runtime, if any. pub last_crash_report: Option, - /// Logic for validating that the app obeys the console's Certification (CAP). pub certifier: Certifier, - /// Pause state: When true, `tick()` will not advance the VM. pub paused: bool, - /// Debugging flag to execute exactly one instruction or frame regardless of budget. pub debug_step_request: bool, - - /// When true, the next logical frame must rearm the entrypoint call before running - /// to avoid resuming at a pending RET after a FRAME_SYNC safe point. - needs_prepare_entry_call: bool, - - /// Wall-clock time of system startup. - boot_time: Instant, + pub(crate) needs_prepare_entry_call: bool, + pub(crate) boot_time: Instant, } impl VirtualMachineRuntime { - /// Default number of cycles assigned to a single game frame. pub const CYCLES_PER_LOGICAL_FRAME: u64 = 100_000; - /// Maximum number of cycles allowed to execute in a single host tick. - /// Usually the same as CYCLES_PER_LOGICAL_FRAME to target 60FPS. pub const SLICE_PER_TICK: u64 = 100_000; - - /// Maximum characters allowed per log message. pub const MAX_LOG_LEN: usize = 256; - /// Maximum log entries an App can emit in a single frame. pub const MAX_LOGS_PER_FRAME: u32 = 10; - - /// Creates a new POS instance with optional certification rules. - pub fn new(cap_config: Option) -> Self { - let boot_time = Instant::now(); - let mut os = Self { - tick_index: 0, - logical_frame_index: 0, - logical_frame_active: false, - logical_frame_remaining_cycles: 0, - last_frame_cpu_time_us: 0, - fs: VirtualFS::new(), - fs_state: FsState::Unmounted, - open_files: HashMap::new(), - next_handle: 1, - log_service: LogService::new(4096), - current_app_id: 0, - current_cartridge_title: String::new(), - current_cartridge_app_version: String::new(), - current_cartridge_app_mode: AppMode::Game, - current_entrypoint: String::new(), - logs_written_this_frame: HashMap::new(), - telemetry_current: TelemetryFrame::default(), - telemetry_last: TelemetryFrame::default(), - last_crash_report: None, - certifier: Certifier::new(cap_config.unwrap_or_default()), - paused: false, - debug_step_request: false, - needs_prepare_entry_call: false, - boot_time, - }; - - os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string()); - - os - } - - pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) { - let ts_ms = self.boot_time.elapsed().as_millis() as u64; - let frame = self.logical_frame_index; - self.log_service.log(ts_ms, frame, level, source, tag, msg); - } - - pub fn mount_fs(&mut self, backend: Box) { - self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string()); - match self.fs.mount(backend) { - Ok(_) => { - self.fs_state = FsState::Mounted; - self.log( - LogLevel::Info, - LogSource::Fs, - 0, - "Filesystem mounted successfully".to_string(), - ); - } - Err(e) => { - let err_msg = format!("Failed to mount filesystem: {:?}", e); - self.log(LogLevel::Error, LogSource::Fs, 0, err_msg); - self.fs_state = FsState::Error(e); - } - } - } - - pub fn unmount_fs(&mut self) { - self.fs.unmount(); - self.fs_state = FsState::Unmounted; - } - - fn update_fs(&mut self) { - if self.fs_state == FsState::Mounted && !self.fs.is_healthy() { - self.log( - LogLevel::Error, - LogSource::Fs, - 0, - "Filesystem became unhealthy, unmounting".to_string(), - ); - self.unmount_fs(); - } - } - - fn clear_cartridge_state(&mut self) { - self.logical_frame_index = 0; - self.logical_frame_active = false; - self.logical_frame_remaining_cycles = 0; - self.last_frame_cpu_time_us = 0; - - self.open_files.clear(); - self.next_handle = 1; - - self.current_app_id = 0; - self.current_cartridge_title.clear(); - self.current_cartridge_app_version.clear(); - self.current_cartridge_app_mode = AppMode::Game; - self.current_entrypoint.clear(); - self.logs_written_this_frame.clear(); - - self.telemetry_current = TelemetryFrame::default(); - self.telemetry_last = TelemetryFrame::default(); - self.last_crash_report = None; - - self.paused = false; - self.debug_step_request = false; - self.needs_prepare_entry_call = false; - } - - pub fn reset(&mut self, vm: &mut VirtualMachine) { - *vm = VirtualMachine::default(); - self.clear_cartridge_state(); - } - - /// Loads a cartridge into the PVM and resets the execution state. - pub fn initialize_vm( - &mut self, - vm: &mut VirtualMachine, - cartridge: &Cartridge, - ) -> Result<(), CrashReport> { - self.clear_cartridge_state(); - vm.set_capabilities(cartridge.capabilities); - - match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) { - Ok(_) => { - // Determines the numeric app_id - self.current_app_id = cartridge.app_id; - self.current_cartridge_title = cartridge.title.clone(); - self.current_cartridge_app_version = cartridge.app_version.clone(); - self.current_cartridge_app_mode = cartridge.app_mode; - self.current_entrypoint = cartridge.entrypoint.clone(); - Ok(()) - } - Err(e) => { - let report = CrashReport::VmInit { error: e }; - self.last_crash_report = Some(report.clone()); - self.log( - LogLevel::Error, - LogSource::Vm, - report.log_tag(), - format!("Failed to initialize VM: {}", report), - ); - // Fail fast: no program is installed, no app id is switched. - // We don't update current_app_id or other fields. - Err(report) - } - } - } - - /// Executes a single VM instruction (Debug). - pub fn debug_step_instruction( - &mut self, - vm: &mut VirtualMachine, - hw: &mut dyn HardwareBridge, - ) -> Option { - let mut ctx = HostContext::new(Some(hw)); - match vm.step(self, &mut ctx) { - Ok(_) => None, - Err(e) => { - let report = match e { - LogicalFrameEndingReason::Trap(trap) => CrashReport::VmTrap { trap }, - LogicalFrameEndingReason::Panic(message) => { - CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) } - } - other => CrashReport::VmPanic { - message: format!("Unexpected fault during step: {:?}", other), - pc: Some(vm.pc() as u32), - }, - }; - self.log( - LogLevel::Error, - LogSource::Vm, - report.log_tag(), - format!("PVM Fault during Step: {}", report), - ); - self.last_crash_report = Some(report.clone()); - Some(report) - } - } - } - - /// Executes a single host tick (nominally 60Hz). - /// - /// This method is responsible for managing the logical frame lifecycle. - /// A single host tick might execute a full logical frame, part of it, - /// or multiple frames depending on the configured slices. - pub fn tick( - &mut self, - vm: &mut VirtualMachine, - signals: &InputSignals, - hw: &mut dyn HardwareBridge, - ) -> Option { - let start = Instant::now(); - self.tick_index += 1; - - // If the system is paused, we don't advance unless there's a debug step request. - if self.paused && !self.debug_step_request { - return None; - } - - self.update_fs(); - - // 1. Frame Initialization - // If we are not currently in the middle of a logical frame, start a new one. - if !self.logical_frame_active { - self.logical_frame_active = true; - self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; - self.begin_logical_frame(signals, hw); - - // If the VM is not currently executing a function (e.g. at the start of the app - // or after the entrypoint function returned), we prepare a new call to the entrypoint. - // Additionally, if the previous slice ended with FRAME_SYNC, we must force a rearm - // so we don't resume execution at a pending RET on the next tick. - if self.needs_prepare_entry_call || vm.call_stack_is_empty() { - vm.prepare_call(&self.current_entrypoint); - self.needs_prepare_entry_call = false; - } - - // Reset telemetry for the new logical frame - self.telemetry_current = TelemetryFrame { - frame_index: self.logical_frame_index, - cycles_budget: self - .certifier - .config - .cycles_budget_per_frame - .unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME), - ..Default::default() - }; - } - - // 2. Budget Allocation - // Determines how many cycles we can run in this host tick. - let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles); - - // 3. VM Execution - if budget > 0 { - // Run the VM until the budget is hit or FRAME_SYNC is reached. - let run_result = { - let mut ctx = HostContext::new(Some(hw)); - vm.run_budget(budget, self, &mut ctx) - }; // internally dispatch to frame on SDK - - match run_result { - Ok(run) => { - self.logical_frame_remaining_cycles = - self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used); - - // Accumulate metrics for telemetry and certification - self.telemetry_current.cycles_used += run.cycles_used; - self.telemetry_current.vm_steps += run.steps_executed; - - // Handle Breakpoints - if run.reason == LogicalFrameEndingReason::Breakpoint { - self.paused = true; - self.debug_step_request = false; - self.log( - LogLevel::Info, - LogSource::Vm, - 0xDEB1, - format!("Breakpoint hit at PC 0x{:X}", vm.pc()), - ); - } - - // Handle Panics - if let LogicalFrameEndingReason::Panic(err) = run.reason { - let report = - CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) }; - self.log( - LogLevel::Error, - LogSource::Vm, - report.log_tag(), - report.summary(), - ); - self.last_crash_report = Some(report.clone()); - return Some(report); - } - - if let LogicalFrameEndingReason::Trap(trap) = &run.reason { - let report = CrashReport::VmTrap { trap: trap.clone() }; - self.log( - LogLevel::Error, - LogSource::Vm, - report.log_tag(), - report.summary(), - ); - self.last_crash_report = Some(report.clone()); - return Some(report); - } - - // 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned) - if run.reason == LogicalFrameEndingReason::FrameSync - || run.reason == LogicalFrameEndingReason::EndOfRom - { - // All drawing commands for this frame are now complete. - // Finalize the framebuffer. - hw.gfx_mut().render_all(); - - // Finalize frame telemetry - self.telemetry_current.host_cpu_time_us = - start.elapsed().as_micros() as u64; - - // Evaluate CAP (Execution Budget Compliance) - let ts_ms = self.boot_time.elapsed().as_millis() as u64; - self.telemetry_current.violations = self.certifier.evaluate( - &self.telemetry_current, - &mut self.log_service, - ts_ms, - ) as u32; - - // Latch telemetry for the Host/Debugger to read. - self.telemetry_current.completed_logical_frames += 1; - self.telemetry_last = self.telemetry_current; - - self.logical_frame_index += 1; - self.logical_frame_active = false; - self.logical_frame_remaining_cycles = 0; - - // If the slice ended in FRAME_SYNC, ensure the next tick starts a fresh - // call to the entrypoint instead of resuming at the RET that follows. - if run.reason == LogicalFrameEndingReason::FrameSync { - self.needs_prepare_entry_call = true; - } - - // If we were doing a "step frame" debug command, pause now that the frame is done. - if self.debug_step_request { - self.paused = true; - self.debug_step_request = false; - } - } - } - Err(e) => { - // Fatal VM fault (division by zero, invalid memory access, etc). - let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) }; - self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary()); - self.last_crash_report = Some(report.clone()); - return Some(report); - } - } - } - - self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; - - // Update bank telemetry in the current frame (snapshot) - let gfx_stats = hw.assets().bank_info(BankType::TILES); - self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes; - self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes; - self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32; - - let audio_stats = hw.assets().bank_info(BankType::SOUNDS); - self.telemetry_current.audio_used_bytes = audio_stats.used_bytes; - self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes; - self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32; - - // If the frame ended exactly in this tick, we update the final real time in the latch. - if !self.logical_frame_active - && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) - { - self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us; - self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget; - self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes; - self.telemetry_last.gfx_inflight_bytes = self.telemetry_current.gfx_inflight_bytes; - self.telemetry_last.gfx_slots_occupied = self.telemetry_current.gfx_slots_occupied; - self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes; - self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes; - self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied; - } - - None - } - - fn begin_logical_frame(&mut self, _signals: &InputSignals, hw: &mut dyn HardwareBridge) { - hw.audio_mut().clear_commands(); - self.logs_written_this_frame.clear(); - } - - // Helper for syscalls - fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> { - let level = match level_val { - 0 => LogLevel::Trace, - 1 => LogLevel::Debug, - 2 => LogLevel::Info, - 3 => LogLevel::Warn, - 4 => LogLevel::Error, - _ => return Err(VmFault::Trap(TRAP_TYPE, format!("Invalid log level: {}", level_val))), - }; - - let app_id = self.current_app_id; - let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0); - - if count >= Self::MAX_LOGS_PER_FRAME { - if count == Self::MAX_LOGS_PER_FRAME { - 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(), - ); - } - return Ok(()); - } - - self.logs_written_this_frame.insert(app_id, count + 1); - - let mut final_msg = msg; - if final_msg.len() > Self::MAX_LOG_LEN { - final_msg.truncate(Self::MAX_LOG_LEN); - } - - self.log(level, LogSource::App { app_id }, tag, final_msg); - - Ok(()) - } - - pub fn get_color(&self, value: i64) -> Color { - // We now use the value directly as RGB565. - Color::from_raw(value as u16) - } - - // Helper for syscalls - pub fn get_button<'a>(&self, id: u32, hw: &'a dyn HardwareBridge) -> Option<&'a Button> { - let pad = hw.pad(); - match id { - 0 => Some(pad.up()), - 1 => Some(pad.down()), - 2 => Some(pad.left()), - 3 => Some(pad.right()), - 4 => Some(pad.a()), - 5 => Some(pad.b()), - 6 => Some(pad.x()), - 7 => Some(pad.y()), - 8 => Some(pad.l()), - 9 => Some(pad.r()), - 10 => Some(pad.start()), - 11 => Some(pad.select()), - _ => None, - } - } - - pub fn is_button_down(&self, id: u32, hw: &mut dyn HardwareBridge) -> bool { - match id { - 0 => hw.pad().up().down, - 1 => hw.pad().down().down, - 2 => hw.pad().left().down, - 3 => hw.pad().right().down, - 4 => hw.pad().a().down, - 5 => hw.pad().b().down, - 6 => hw.pad().x().down, - 7 => hw.pad().y().down, - 8 => hw.pad().l().down, - 9 => hw.pad().r().down, - 10 => hw.pad().start().down, - 11 => hw.pad().select().down, - _ => false, - } - } -} - -impl NativeInterface for VirtualMachineRuntime { - /// Dispatches a syscall from the VM to the native implementation. - /// - /// Syscalls are grouped by functionality: - /// - 0x0000: System/Cartridge management - /// - 0x1000: Graphics (GFX) - /// - 0x2000: Input - /// - 0x3000: Audio - /// - 0x4000: Filesystem (FS) - /// - 0x5000: Logging - /// - /// Each syscall returns the number of virtual cycles it consumed. - fn syscall( - &mut self, - id: SyscallId, - args: &[Value], - ret: &mut HostReturn, - ctx: &mut HostContext, - ) -> Result<(), VmFault> { - self.telemetry_current.syscalls += 1; - let syscall = Syscall::from_u32(id).ok_or_else(|| { - VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id)) - })?; - - // Handle hardware-less syscalls first - match syscall { - Syscall::SystemHasCart => { - ret.push_bool(true); - return Ok(()); - } - Syscall::SystemRunCart => { - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - return Ok(()); - } - _ => {} - } - - let hw = ctx.require_hw()?; - - match syscall { - // --- System Syscalls --- - Syscall::SystemHasCart => unreachable!(), - Syscall::SystemRunCart => unreachable!(), - - // --- GFX Syscalls --- - - // gfx.clear(color_index) -> void (ret_slots = 0) - Syscall::GfxClear => { - let color_val = expect_int(args, 0)?; - let color = self.get_color(color_val); - hw.gfx_mut().clear(color); - Ok(()) - } - // gfx.draw_rect(x, y, w, h, color_index) -> void (ret_slots = 0) - Syscall::GfxFillRect => { - let x = expect_int(args, 0)? as i32; - let y = expect_int(args, 1)? as i32; - let w = expect_int(args, 2)? as i32; - let h = expect_int(args, 3)? as i32; - let color_val = expect_int(args, 4)?; - let color = self.get_color(color_val); - hw.gfx_mut().fill_rect(x, y, w, h, color); - Ok(()) - } - // gfx.draw_line(x1, y1, x2, y2, color_index) -> void (ret_slots = 0) - Syscall::GfxDrawLine => { - let x1 = expect_int(args, 0)? as i32; - let y1 = expect_int(args, 1)? as i32; - let x2 = expect_int(args, 2)? as i32; - let y2 = expect_int(args, 3)? as i32; - let color_val = expect_int(args, 4)?; - let color = self.get_color(color_val); - hw.gfx_mut().draw_line(x1, y1, x2, y2, color); - Ok(()) - } - // gfx.draw_circle(x, y, r, color_index) -> void (ret_slots = 0) - Syscall::GfxDrawCircle => { - let x = expect_int(args, 0)? as i32; - let y = expect_int(args, 1)? as i32; - let r = expect_int(args, 2)? as i32; - let color_val = expect_int(args, 3)?; - let color = self.get_color(color_val); - hw.gfx_mut().draw_circle(x, y, r, color); - Ok(()) - } - // gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) -> void (ret_slots = 0) - Syscall::GfxDrawDisc => { - let x = expect_int(args, 0)? as i32; - let y = expect_int(args, 1)? as i32; - let r = expect_int(args, 2)? as i32; - let border_color_val = expect_int(args, 3)?; - let fill_color_val = expect_int(args, 4)?; - let fill_color = self.get_color(fill_color_val); - let border_color = self.get_color(border_color_val); - hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); - Ok(()) - } - // gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) -> void (ret_slots = 0) - Syscall::GfxDrawSquare => { - let x = expect_int(args, 0)? as i32; - let y = expect_int(args, 1)? as i32; - let w = expect_int(args, 2)? as i32; - let h = expect_int(args, 3)? as i32; - let border_color_val = expect_int(args, 4)?; - let fill_color_val = expect_int(args, 5)?; - let fill_color = self.get_color(fill_color_val); - let border_color = self.get_color(border_color_val); - hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); - Ok(()) - } - // gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority) - Syscall::GfxSetSprite => { - let asset_name = match args - .first() - .ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())), - }; - let index = expect_int(args, 1)? as usize; - let x = expect_int(args, 2)? as i32; - let y = expect_int(args, 3)? as i32; - let tile_id = expect_int(args, 4)? as u16; - let palette_id = expect_int(args, 5)? as u8; - let active = expect_bool(args, 6)?; - let flip_x = expect_bool(args, 7)?; - let flip_y = expect_bool(args, 8)?; - let priority = expect_int(args, 9)? as u8; - - let bank_id = - hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0); - - if index < 512 { - *hw.gfx_mut().sprite_mut(index) = Sprite { - tile: Tile { id: tile_id, flip_x: false, flip_y: false, palette_id }, - x, - y, - bank_id, - active, - flip_x, - flip_y, - priority, - }; - } - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - Syscall::GfxDrawText => { - let x = expect_int(args, 0)? as i32; - let y = expect_int(args, 1)? as i32; - let msg = match args - .get(2) - .ok_or_else(|| VmFault::Panic("Missing message".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), - }; - let color_val = expect_int(args, 3)?; - let color = self.get_color(color_val); - hw.gfx_mut().draw_text(x, y, &msg, color); - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - // gfx.clear565(color_u16) -> void - Syscall::GfxClear565 => { - let color_val = expect_int(args, 0)? as u32; - if color_val > 0xFFFF { - return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds".into())); - } - let color = Color::from_raw(color_val as u16); - hw.gfx_mut().clear(color); - // No return value for void - Ok(()) - } - - // --- Input Syscalls --- - - // input.get_pad(button_id) -> bool - Syscall::InputGetPad => { - let button_id = expect_int(args, 0)? as u32; - let is_down = self.is_button_down(button_id, hw); - ret.push_bool(is_down); - Ok(()) - } - Syscall::InputGetPadPressed => { - let button_id = expect_int(args, 0)? as u32; - let val = self.get_button(button_id, hw).map(|b| b.pressed).unwrap_or(false); - ret.push_bool(val); - Ok(()) - } - Syscall::InputGetPadReleased => { - let button_id = expect_int(args, 0)? as u32; - let val = self.get_button(button_id, hw).map(|b| b.released).unwrap_or(false); - ret.push_bool(val); - Ok(()) - } - Syscall::InputGetPadHold => { - 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); - ret.push_int(val as i64); - Ok(()) - } - - Syscall::TouchGetX => { - ret.push_int(hw.touch().x() as i64); - Ok(()) - } - Syscall::TouchGetY => { - ret.push_int(hw.touch().y() as i64); - Ok(()) - } - Syscall::TouchIsDown => { - ret.push_bool(hw.touch().f().down); - Ok(()) - } - Syscall::TouchIsPressed => { - ret.push_bool(hw.touch().f().pressed); - Ok(()) - } - Syscall::TouchIsReleased => { - ret.push_bool(hw.touch().f().released); - Ok(()) - } - Syscall::TouchGetHold => { - ret.push_int(hw.touch().f().hold_frames as i64); - Ok(()) - } - Syscall::TouchGetFinger => { - let btn = hw.touch().f(); - // Return as 4 slots to mirror snapshot order - ret.push_bool(btn.pressed); - ret.push_bool(btn.released); - ret.push_bool(btn.down); - ret.push_int(btn.hold_frames as i64); - Ok(()) - } - Syscall::InputPadSnapshot => { - let pad = hw.pad(); - for btn in [ - pad.up(), - pad.down(), - pad.left(), - pad.right(), - pad.a(), - pad.b(), - pad.x(), - pad.y(), - pad.l(), - pad.r(), - pad.start(), - pad.select(), - ] { - ret.push_bool(btn.pressed); - ret.push_bool(btn.released); - ret.push_bool(btn.down); - ret.push_int(btn.hold_frames as i64); - } - Ok(()) - } - Syscall::InputTouchSnapshot => { - let touch = hw.touch(); - ret.push_bool(touch.f().pressed); - ret.push_bool(touch.f().released); - ret.push_bool(touch.f().down); - ret.push_int(touch.f().hold_frames as i64); - ret.push_int(touch.x() as i64); - ret.push_int(touch.y() as i64); - Ok(()) - } - - // --- Pad per-button service-based syscalls (return Button as 4 slots) --- - Syscall::PadGetUp => { - let b = hw.pad().up(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetDown => { - let b = hw.pad().down(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetLeft => { - let b = hw.pad().left(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetRight => { - let b = hw.pad().right(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetA => { - let b = hw.pad().a(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetB => { - let b = hw.pad().b(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetX => { - let b = hw.pad().x(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetY => { - let b = hw.pad().y(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetL => { - let b = hw.pad().l(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetR => { - let b = hw.pad().r(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetStart => { - let b = hw.pad().start(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - Syscall::PadGetSelect => { - let b = hw.pad().select(); - ret.push_bool(b.pressed); - ret.push_bool(b.released); - ret.push_bool(b.down); - ret.push_int(b.hold_frames as i64); - Ok(()) - } - - // --- Audio Syscalls --- - - // audio.play_sample(sample_id, voice_id, volume, pan, pitch) - Syscall::AudioPlaySample => { - let sample_id = expect_int(args, 0)? as u32; - let voice_id = expect_int(args, 1)? as usize; - let volume = expect_int(args, 2)? as u8; - let pan = expect_int(args, 3)? as u8; - let pitch = match args - .get(4) - .ok_or_else(|| VmFault::Panic("Missing pitch".into()))? - { - Value::Float(f) => *f, - Value::Int32(i) => *i as f64, - Value::Int64(i) => *i as f64, - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected number for pitch".into())), - }; - - hw.audio_mut().play( - 0, - sample_id as u16, - voice_id, - volume, - pan, - pitch, - 0, - prometeu_hal::LoopMode::Off, - ); - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - - // audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode) - Syscall::AudioPlay => { - let asset_name = match args - .first() - .ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(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, - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected number for pitch".into())), - }; - let loop_mode = match expect_int(args, 6)? { - 0 => prometeu_hal::LoopMode::Off, - _ => prometeu_hal::LoopMode::On, - }; - - let bank_id = - hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0); - - hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode); - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - - // --- Filesystem Syscalls (0x4000) --- - - // FS_OPEN(path) -> handle - Syscall::FsOpen => { - let path = - match args.first().ok_or_else(|| VmFault::Panic("Missing path".into()))? { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())), - }; - if self.fs_state != FsState::Mounted { - ret.push_int(-1); - return Ok(()); - } - let handle = self.next_handle; - self.open_files.insert(handle, path); - self.next_handle += 1; - ret.push_int(handle as i64); - Ok(()) - } - // FS_READ(handle) -> content - Syscall::FsRead => { - let handle = expect_int(args, 0)? as u32; - let path = self - .open_files - .get(&handle) - .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; - match self.fs.read_file(path) { - Ok(data) => { - let s = String::from_utf8_lossy(&data).into_owned(); - ret.push_string(s); - } - Err(_) => ret.push_null(), - } - Ok(()) - } - // FS_WRITE(handle, content) - Syscall::FsWrite => { - let handle = expect_int(args, 0)? as u32; - let content = match args - .get(1) - .ok_or_else(|| VmFault::Panic("Missing content".into()))? - { - Value::String(s) => s.as_bytes().to_vec(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string content".into())), - }; - let path = self - .open_files - .get(&handle) - .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; - match self.fs.write_file(path, &content) { - Ok(_) => ret.push_bool(true), - Err(_) => ret.push_bool(false), - } - Ok(()) - } - // FS_CLOSE(handle) - Syscall::FsClose => { - let handle = expect_int(args, 0)? as u32; - self.open_files.remove(&handle); - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - // FS_LIST_DIR(path) - Syscall::FsListDir => { - let path = - match args.first().ok_or_else(|| VmFault::Panic("Missing path".into()))? { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())), - }; - match self.fs.list_dir(&path) { - Ok(entries) => { - let names: Vec = entries.into_iter().map(|e| e.name).collect(); - ret.push_string(names.join(";")); - } - Err(_) => ret.push_null(), - } - Ok(()) - } - // FS_EXISTS(path) - Syscall::FsExists => { - let path = - match args.first().ok_or_else(|| VmFault::Panic("Missing path".into()))? { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())), - }; - ret.push_bool(self.fs.exists(&path)); - Ok(()) - } - // FS_DELETE(path) - Syscall::FsDelete => { - let path = - match args.first().ok_or_else(|| VmFault::Panic("Missing path".into()))? { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())), - }; - match self.fs.delete(&path) { - Ok(_) => ret.push_bool(true), - Err(_) => ret.push_bool(false), - } - Ok(()) - } - - // --- Log Syscalls (0x5000) --- - - // LOG_WRITE(level, msg) - Syscall::LogWrite => { - let level = expect_int(args, 0)?; - let msg = match args - .get(1) - .ok_or_else(|| VmFault::Panic("Missing message".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), - }; - self.syscall_log_write(level, 0, msg)?; - // void - Ok(()) - } - // LOG_WRITE_TAG(level, tag, msg) - Syscall::LogWriteTag => { - let level = expect_int(args, 0)?; - let tag = expect_int(args, 1)? as u16; - let msg = match args - .get(2) - .ok_or_else(|| VmFault::Panic("Missing message".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), - }; - self.syscall_log_write(level, tag, msg)?; - // void - Ok(()) - } - - // --- Asset Syscalls --- - Syscall::AssetLoad => { - let asset_id = match args - .first() - .ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_id".into())), - }; - let asset_type_val = expect_int(args, 1)? as u32; - let slot_index = expect_int(args, 2)? as usize; - - let asset_type = match asset_type_val { - 0 => BankType::TILES, - 1 => BankType::SOUNDS, - _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), - }; - let slot = SlotRef { asset_type, index: slot_index }; - - match hw.assets().load(&asset_id, slot) { - Ok(handle) => { - ret.push_int(handle as i64); - Ok(()) - } - Err(e) => Err(VmFault::Panic(e)), - } - } - Syscall::AssetStatus => { - let handle = expect_int(args, 0)? as u32; - let status = hw.assets().status(handle); - let status_val = match status { - LoadStatus::PENDING => 0, - LoadStatus::LOADING => 1, - LoadStatus::READY => 2, - LoadStatus::COMMITTED => 3, - LoadStatus::CANCELED => 4, - LoadStatus::ERROR => 5, - }; - ret.push_int(status_val); - Ok(()) - } - Syscall::AssetCommit => { - let handle = expect_int(args, 0)? as u32; - hw.assets().commit(handle); - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - Syscall::AssetCancel => { - let handle = expect_int(args, 0)? as u32; - hw.assets().cancel(handle); - // ret_slots = 0 (void). Must return exactly 0 values per SyscallMeta. - Ok(()) - } - Syscall::BankInfo => { - let asset_type_val = expect_int(args, 0)? as u32; - let asset_type = match asset_type_val { - 0 => BankType::TILES, - 1 => BankType::SOUNDS, - _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), - }; - let info = hw.assets().bank_info(asset_type); - let json = serde_json::to_string(&info).unwrap_or_default(); - ret.push_string(json); - Ok(()) - } - Syscall::BankSlotInfo => { - let asset_type_val = expect_int(args, 0)? as u32; - let slot_index = expect_int(args, 1)? as usize; - let asset_type = match asset_type_val { - 0 => BankType::TILES, - 1 => BankType::SOUNDS, - _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), - }; - let slot = SlotRef { asset_type, index: slot_index }; - let info = hw.assets().slot_info(slot); - let json = serde_json::to_string(&info).unwrap_or_default(); - ret.push_string(json); - Ok(()) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use prometeu_bytecode::TRAP_TYPE; - use prometeu_bytecode::assembler::assemble; - use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; - use prometeu_drivers::hardware::Hardware; - use prometeu_hal::InputSignals; - use prometeu_hal::cartridge::Cartridge; - use prometeu_hal::syscalls::caps; - use prometeu_vm::VmInitError; - - fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { - Cartridge { - app_id: 42, - title: "Test Cart".into(), - app_version: "1.0.0".into(), - app_mode: AppMode::Game, - entrypoint: "".into(), - capabilities, - program, - assets: vec![], - asset_table: vec![], - preload: vec![], - } - } - - fn serialized_single_function_module(code: Vec, syscalls: Vec) -> Vec { - BytecodeModule { - version: 0, - const_pool: vec![], - functions: vec![FunctionMeta { - code_offset: 0, - code_len: code.len() as u32, - ..Default::default() - }], - code, - debug_info: None, - exports: vec![], - syscalls, - } - .serialize() - } - - #[test] - fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { - let mut runtime = VirtualMachineRuntime::new(None); - let mut vm = VirtualMachine::default(); - let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); - let program = serialized_single_function_module( - code, - vec![SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }], - ); - let cartridge = cartridge_with_program(program, caps::NONE); - - let res = runtime.initialize_vm(&mut vm, &cartridge); - - assert!(matches!( - res, - Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }) - )); - assert_eq!(runtime.current_app_id, 0); - assert_eq!(vm.pc(), 0); - assert_eq!(vm.operand_stack_top(1), Vec::::new()); - } - - #[test] - fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { - let mut runtime = VirtualMachineRuntime::new(None); - let mut vm = VirtualMachine::default(); - let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); - let program = serialized_single_function_module( - code, - vec![SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }], - ); - let cartridge = cartridge_with_program(program, caps::GFX); - - let res = runtime.initialize_vm(&mut vm, &cartridge); - - assert!(res.is_ok()); - assert_eq!(runtime.current_app_id, 42); - assert!(!vm.is_halted()); - } - - #[test] - fn tick_returns_error_when_vm_ends_slice_with_trap() { - let mut runtime = VirtualMachineRuntime::new(None); - let mut vm = VirtualMachine::default(); - let mut hardware = Hardware::new(); - let signals = InputSignals::default(); - let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble"); - let program = serialized_single_function_module( - code, - vec![SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }], - ); - let cartridge = cartridge_with_program(program, caps::GFX); - - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - - let report = runtime - .tick(&mut vm, &signals, &mut hardware) - .expect("trap must surface as runtime error"); - - match report { - CrashReport::VmTrap { trap } => { - assert_eq!(trap.code, TRAP_TYPE); - assert!(trap.message.contains("Expected integer at index 0")); - } - other => panic!("expected VmTrap crash report, got {:?}", other), - } - assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); - } - - #[test] - fn tick_returns_panic_report_distinct_from_trap() { - let mut runtime = VirtualMachineRuntime::new(None); - let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); - - let mut hardware = Hardware::new(); - let signals = InputSignals::default(); - - let report = runtime - .tick(&mut vm, &signals, &mut hardware) - .expect("panic must surface as runtime error"); - - match report { - CrashReport::VmPanic { message, pc } => { - assert!(message.contains("HOSTCALL 0 reached execution without loader patching")); - assert!(pc.is_some()); - } - other => panic!("expected VmPanic crash report, got {:?}", other), - } - assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. }))); - } - - #[test] - fn initialize_vm_success_clears_previous_crash_report() { - let mut runtime = VirtualMachineRuntime::new(None); - runtime.last_crash_report = - Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) }); - - let mut vm = VirtualMachine::default(); - let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); - let program = serialized_single_function_module( - code, - vec![SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }], - ); - let cartridge = cartridge_with_program(program, caps::GFX); - - runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); - assert!(runtime.last_crash_report.is_none()); - } - - #[test] - fn reset_clears_cartridge_scoped_runtime_state() { - let mut runtime = VirtualMachineRuntime::new(None); - let mut vm = VirtualMachine::default(); - - runtime.tick_index = 77; - runtime.logical_frame_index = 12; - runtime.logical_frame_active = true; - runtime.logical_frame_remaining_cycles = 345; - runtime.last_frame_cpu_time_us = 678; - runtime.fs_state = FsState::Mounted; - runtime.open_files.insert(7, "/save.dat".into()); - runtime.next_handle = 9; - runtime.current_app_id = 42; - runtime.current_cartridge_title = "Cart".into(); - runtime.current_cartridge_app_version = "1.2.3".into(); - runtime.current_cartridge_app_mode = AppMode::System; - runtime.current_entrypoint = "main".into(); - runtime.logs_written_this_frame.insert(42, 3); - runtime.telemetry_current.frame_index = 8; - runtime.telemetry_current.cycles_used = 99; - runtime.telemetry_last.frame_index = 7; - runtime.telemetry_last.completed_logical_frames = 2; - runtime.last_crash_report = - Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(55) }); - runtime.paused = true; - runtime.debug_step_request = true; - runtime.needs_prepare_entry_call = true; - - runtime.reset(&mut vm); - - assert_eq!(runtime.tick_index, 77); - assert_eq!(runtime.fs_state, FsState::Mounted); - assert_eq!(runtime.logical_frame_index, 0); - assert!(!runtime.logical_frame_active); - assert_eq!(runtime.logical_frame_remaining_cycles, 0); - assert_eq!(runtime.last_frame_cpu_time_us, 0); - assert!(runtime.open_files.is_empty()); - assert_eq!(runtime.next_handle, 1); - assert_eq!(runtime.current_app_id, 0); - assert!(runtime.current_cartridge_title.is_empty()); - assert!(runtime.current_cartridge_app_version.is_empty()); - assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); - assert!(runtime.current_entrypoint.is_empty()); - assert!(runtime.logs_written_this_frame.is_empty()); - assert_eq!(runtime.telemetry_current.frame_index, 0); - assert_eq!(runtime.telemetry_current.cycles_used, 0); - assert_eq!(runtime.telemetry_last.frame_index, 0); - assert_eq!(runtime.telemetry_last.completed_logical_frames, 0); - assert!(runtime.last_crash_report.is_none()); - assert!(!runtime.paused); - assert!(!runtime.debug_step_request); - assert!(!runtime.needs_prepare_entry_call); - assert_eq!(vm.pc(), 0); - } - - #[test] - fn initialize_vm_failure_clears_previous_identity_and_handles() { - let mut runtime = VirtualMachineRuntime::new(None); - let mut vm = VirtualMachine::default(); - - let good_program = serialized_single_function_module( - assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), - vec![SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }], - ); - let good_cartridge = cartridge_with_program(good_program, caps::GFX); - runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize"); - - runtime.open_files.insert(5, "/save.dat".into()); - runtime.next_handle = 6; - runtime.paused = true; - runtime.debug_step_request = true; - runtime.telemetry_current.cycles_used = 123; - - let bad_program = serialized_single_function_module( - assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), - vec![SyscallDecl { - module: "gfx".into(), - name: "clear".into(), - version: 1, - arg_slots: 1, - ret_slots: 0, - }], - ); - let bad_cartridge = cartridge_with_program(bad_program, caps::NONE); - - let res = runtime.initialize_vm(&mut vm, &bad_cartridge); - - assert!(matches!( - res, - Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }) - )); - assert_eq!(runtime.current_app_id, 0); - assert!(runtime.current_cartridge_title.is_empty()); - assert!(runtime.current_cartridge_app_version.is_empty()); - assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); - assert!(runtime.current_entrypoint.is_empty()); - assert!(runtime.open_files.is_empty()); - assert_eq!(runtime.next_handle, 1); - assert!(!runtime.paused); - assert!(!runtime.debug_step_request); - assert_eq!(runtime.telemetry_current.cycles_used, 0); - assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmInit { .. }))); - } } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs new file mode 100644 index 00000000..f3a466e5 --- /dev/null +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -0,0 +1,546 @@ +use super::*; +use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; +use prometeu_hal::asset::{BankType, LoadStatus, SlotRef}; +use prometeu_hal::button::Button; +use prometeu_hal::color::Color; +use prometeu_hal::log::{LogLevel, LogSource}; +use prometeu_hal::sprite::Sprite; +use prometeu_hal::syscalls::Syscall; +use prometeu_hal::tile::Tile; +use prometeu_hal::vm_fault::VmFault; +use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; + +impl VirtualMachineRuntime { + fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> { + let level = match level_val { + 0 => LogLevel::Trace, + 1 => LogLevel::Debug, + 2 => LogLevel::Info, + 3 => LogLevel::Warn, + 4 => LogLevel::Error, + _ => return Err(VmFault::Trap(TRAP_TYPE, format!("Invalid log level: {}", level_val))), + }; + + let app_id = self.current_app_id; + let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0); + + if count >= Self::MAX_LOGS_PER_FRAME { + if count == Self::MAX_LOGS_PER_FRAME { + 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(), + ); + } + return Ok(()); + } + + self.logs_written_this_frame.insert(app_id, count + 1); + + let mut final_msg = msg; + if final_msg.len() > Self::MAX_LOG_LEN { + final_msg.truncate(Self::MAX_LOG_LEN); + } + + self.log(level, LogSource::App { app_id }, tag, final_msg); + Ok(()) + } + + pub(crate) fn get_color(&self, value: i64) -> Color { + Color::from_raw(value as u16) + } + + pub(crate) fn get_button<'a>( + &self, + id: u32, + hw: &'a dyn prometeu_hal::HardwareBridge, + ) -> Option<&'a Button> { + let pad = hw.pad(); + match id { + 0 => Some(pad.up()), + 1 => Some(pad.down()), + 2 => Some(pad.left()), + 3 => Some(pad.right()), + 4 => Some(pad.a()), + 5 => Some(pad.b()), + 6 => Some(pad.x()), + 7 => Some(pad.y()), + 8 => Some(pad.l()), + 9 => Some(pad.r()), + 10 => Some(pad.start()), + 11 => Some(pad.select()), + _ => None, + } + } + + pub(crate) fn is_button_down( + &self, + id: u32, + hw: &mut dyn prometeu_hal::HardwareBridge, + ) -> bool { + match id { + 0 => hw.pad().up().down, + 1 => hw.pad().down().down, + 2 => hw.pad().left().down, + 3 => hw.pad().right().down, + 4 => hw.pad().a().down, + 5 => hw.pad().b().down, + 6 => hw.pad().x().down, + 7 => hw.pad().y().down, + 8 => hw.pad().l().down, + 9 => hw.pad().r().down, + 10 => hw.pad().start().down, + 11 => hw.pad().select().down, + _ => false, + } + } +} + +impl NativeInterface for VirtualMachineRuntime { + fn syscall( + &mut self, + id: SyscallId, + args: &[Value], + ret: &mut HostReturn, + ctx: &mut HostContext, + ) -> Result<(), VmFault> { + self.telemetry_current.syscalls += 1; + let syscall = Syscall::from_u32(id).ok_or_else(|| { + VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id)) + })?; + + match syscall { + Syscall::SystemHasCart => { + ret.push_bool(true); + return Ok(()); + } + Syscall::SystemRunCart => return Ok(()), + _ => {} + } + + let hw = ctx.require_hw()?; + + match syscall { + Syscall::SystemHasCart => unreachable!(), + Syscall::SystemRunCart => unreachable!(), + Syscall::GfxClear => { + let color = self.get_color(expect_int(args, 0)?); + hw.gfx_mut().clear(color); + Ok(()) + } + Syscall::GfxFillRect => { + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let w = expect_int(args, 2)? as i32; + let h = expect_int(args, 3)? as i32; + let color = self.get_color(expect_int(args, 4)?); + hw.gfx_mut().fill_rect(x, y, w, h, color); + Ok(()) + } + Syscall::GfxDrawLine => { + let x1 = expect_int(args, 0)? as i32; + let y1 = expect_int(args, 1)? as i32; + let x2 = expect_int(args, 2)? as i32; + let y2 = expect_int(args, 3)? as i32; + let color = self.get_color(expect_int(args, 4)?); + hw.gfx_mut().draw_line(x1, y1, x2, y2, color); + Ok(()) + } + Syscall::GfxDrawCircle => { + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let r = expect_int(args, 2)? as i32; + let color = self.get_color(expect_int(args, 3)?); + hw.gfx_mut().draw_circle(x, y, r, color); + Ok(()) + } + Syscall::GfxDrawDisc => { + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let r = expect_int(args, 2)? as i32; + let border_color = self.get_color(expect_int(args, 3)?); + let fill_color = self.get_color(expect_int(args, 4)?); + hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); + Ok(()) + } + Syscall::GfxDrawSquare => { + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let w = expect_int(args, 2)? as i32; + let h = expect_int(args, 3)? as i32; + let border_color = self.get_color(expect_int(args, 4)?); + let fill_color = self.get_color(expect_int(args, 5)?); + hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); + Ok(()) + } + Syscall::GfxSetSprite => { + let asset_name = match args + .first() + .ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? + { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())), + }; + let index = expect_int(args, 1)? as usize; + let x = expect_int(args, 2)? as i32; + let y = expect_int(args, 3)? as i32; + let tile_id = expect_int(args, 4)? as u16; + let palette_id = expect_int(args, 5)? as u8; + let active = expect_bool(args, 6)?; + let flip_x = expect_bool(args, 7)?; + let flip_y = expect_bool(args, 8)?; + let priority = expect_int(args, 9)? as u8; + + let bank_id = + hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0); + + if index < 512 { + *hw.gfx_mut().sprite_mut(index) = Sprite { + tile: Tile { id: tile_id, flip_x: false, flip_y: false, palette_id }, + x, + y, + bank_id, + active, + flip_x, + flip_y, + priority, + }; + } + Ok(()) + } + Syscall::GfxDrawText => { + let x = expect_int(args, 0)? as i32; + let y = expect_int(args, 1)? as i32; + let msg = match args + .get(2) + .ok_or_else(|| VmFault::Panic("Missing message".into()))? + { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), + }; + let color = self.get_color(expect_int(args, 3)?); + hw.gfx_mut().draw_text(x, y, &msg, color); + Ok(()) + } + Syscall::GfxClear565 => { + let color_val = expect_int(args, 0)? as u32; + if color_val > 0xFFFF { + return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds".into())); + } + hw.gfx_mut().clear(Color::from_raw(color_val as u16)); + Ok(()) + } + Syscall::InputGetPad => { + ret.push_bool(self.is_button_down(expect_int(args, 0)? as u32, hw)); + Ok(()) + } + Syscall::InputGetPadPressed => { + let val = self + .get_button(expect_int(args, 0)? as u32, hw) + .map(|b| b.pressed) + .unwrap_or(false); + ret.push_bool(val); + Ok(()) + } + Syscall::InputGetPadReleased => { + let val = self + .get_button(expect_int(args, 0)? as u32, hw) + .map(|b| b.released) + .unwrap_or(false); + ret.push_bool(val); + Ok(()) + } + Syscall::InputGetPadHold => { + let val = self + .get_button(expect_int(args, 0)? as u32, hw) + .map(|b| b.hold_frames) + .unwrap_or(0); + ret.push_int(val as i64); + Ok(()) + } + Syscall::TouchGetX => { + ret.push_int(hw.touch().x() as i64); + Ok(()) + } + Syscall::TouchGetY => { + ret.push_int(hw.touch().y() as i64); + Ok(()) + } + Syscall::TouchIsDown => { + ret.push_bool(hw.touch().f().down); + Ok(()) + } + Syscall::TouchIsPressed => { + ret.push_bool(hw.touch().f().pressed); + Ok(()) + } + Syscall::TouchIsReleased => { + ret.push_bool(hw.touch().f().released); + Ok(()) + } + Syscall::TouchGetHold => { + ret.push_int(hw.touch().f().hold_frames as i64); + Ok(()) + } + Syscall::TouchGetFinger => { + let btn = hw.touch().f(); + ret.push_bool(btn.pressed); + ret.push_bool(btn.released); + ret.push_bool(btn.down); + ret.push_int(btn.hold_frames as i64); + Ok(()) + } + Syscall::InputPadSnapshot => { + let pad = hw.pad(); + for btn in [ + pad.up(), + pad.down(), + pad.left(), + pad.right(), + pad.a(), + pad.b(), + pad.x(), + pad.y(), + pad.l(), + pad.r(), + pad.start(), + pad.select(), + ] { + ret.push_bool(btn.pressed); + ret.push_bool(btn.released); + ret.push_bool(btn.down); + ret.push_int(btn.hold_frames as i64); + } + Ok(()) + } + Syscall::InputTouchSnapshot => { + let touch = hw.touch(); + ret.push_bool(touch.f().pressed); + ret.push_bool(touch.f().released); + ret.push_bool(touch.f().down); + ret.push_int(touch.f().hold_frames as i64); + ret.push_int(touch.x() as i64); + ret.push_int(touch.y() as i64); + Ok(()) + } + Syscall::PadGetUp => push_button(ret, hw.pad().up()), + Syscall::PadGetDown => push_button(ret, hw.pad().down()), + Syscall::PadGetLeft => push_button(ret, hw.pad().left()), + Syscall::PadGetRight => push_button(ret, hw.pad().right()), + Syscall::PadGetA => push_button(ret, hw.pad().a()), + Syscall::PadGetB => push_button(ret, hw.pad().b()), + Syscall::PadGetX => push_button(ret, hw.pad().x()), + Syscall::PadGetY => push_button(ret, hw.pad().y()), + Syscall::PadGetL => push_button(ret, hw.pad().l()), + Syscall::PadGetR => push_button(ret, hw.pad().r()), + Syscall::PadGetStart => push_button(ret, hw.pad().start()), + Syscall::PadGetSelect => push_button(ret, hw.pad().select()), + Syscall::AudioPlaySample => { + let sample_id = expect_int(args, 0)? as u32; + let voice_id = expect_int(args, 1)? as usize; + let volume = expect_int(args, 2)? as u8; + let pan = expect_int(args, 3)? as u8; + let pitch = expect_number(args, 4, "pitch")?; + + hw.audio_mut().play( + 0, + sample_id as u16, + voice_id, + volume, + pan, + pitch, + 0, + prometeu_hal::LoopMode::Off, + ); + Ok(()) + } + Syscall::AudioPlay => { + let asset_name = match args + .first() + .ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? + { + Value::String(s) => s.clone(), + _ => return Err(VmFault::Trap(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 = expect_number(args, 5, "pitch")?; + let loop_mode = match expect_int(args, 6)? { + 0 => prometeu_hal::LoopMode::Off, + _ => prometeu_hal::LoopMode::On, + }; + + let bank_id = + hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0); + hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode); + Ok(()) + } + Syscall::FsOpen => { + let path = expect_string(args, 0, "path")?; + if self.fs_state != FsState::Mounted { + ret.push_int(-1); + return Ok(()); + } + let handle = self.next_handle; + self.open_files.insert(handle, path); + self.next_handle += 1; + ret.push_int(handle as i64); + Ok(()) + } + Syscall::FsRead => { + let handle = expect_int(args, 0)? as u32; + let path = self + .open_files + .get(&handle) + .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; + match self.fs.read_file(path) { + Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()), + Err(_) => ret.push_null(), + } + Ok(()) + } + Syscall::FsWrite => { + let handle = expect_int(args, 0)? as u32; + let content = expect_string(args, 1, "content")?.into_bytes(); + let path = self + .open_files + .get(&handle) + .ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; + match self.fs.write_file(path, &content) { + Ok(_) => ret.push_bool(true), + Err(_) => ret.push_bool(false), + } + Ok(()) + } + Syscall::FsClose => { + self.open_files.remove(&(expect_int(args, 0)? as u32)); + Ok(()) + } + Syscall::FsListDir => { + let path = expect_string(args, 0, "path")?; + match self.fs.list_dir(&path) { + Ok(entries) => { + let names: Vec = entries.into_iter().map(|e| e.name).collect(); + ret.push_string(names.join(";")); + } + Err(_) => ret.push_null(), + } + Ok(()) + } + Syscall::FsExists => { + ret.push_bool(self.fs.exists(&expect_string(args, 0, "path")?)); + Ok(()) + } + Syscall::FsDelete => { + match self.fs.delete(&expect_string(args, 0, "path")?) { + Ok(_) => ret.push_bool(true), + Err(_) => ret.push_bool(false), + } + Ok(()) + } + Syscall::LogWrite => { + self.syscall_log_write( + expect_int(args, 0)?, + 0, + expect_string(args, 1, "message")?, + )?; + Ok(()) + } + Syscall::LogWriteTag => { + self.syscall_log_write( + expect_int(args, 0)?, + expect_int(args, 1)? as u16, + expect_string(args, 2, "message")?, + )?; + Ok(()) + } + Syscall::AssetLoad => { + let asset_id = expect_string(args, 0, "asset_id")?; + let asset_type = match expect_int(args, 1)? as u32 { + 0 => BankType::TILES, + 1 => BankType::SOUNDS, + _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), + }; + let slot = SlotRef { asset_type, index: expect_int(args, 2)? as usize }; + + match hw.assets().load(&asset_id, slot) { + Ok(handle) => { + ret.push_int(handle as i64); + Ok(()) + } + Err(e) => Err(VmFault::Panic(e)), + } + } + Syscall::AssetStatus => { + let status_val = match hw.assets().status(expect_int(args, 0)? as u32) { + LoadStatus::PENDING => 0, + LoadStatus::LOADING => 1, + LoadStatus::READY => 2, + LoadStatus::COMMITTED => 3, + LoadStatus::CANCELED => 4, + LoadStatus::ERROR => 5, + }; + ret.push_int(status_val); + Ok(()) + } + Syscall::AssetCommit => { + hw.assets().commit(expect_int(args, 0)? as u32); + Ok(()) + } + Syscall::AssetCancel => { + hw.assets().cancel(expect_int(args, 0)? as u32); + Ok(()) + } + Syscall::BankInfo => { + let asset_type = match expect_int(args, 0)? as u32 { + 0 => BankType::TILES, + 1 => BankType::SOUNDS, + _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), + }; + let json = + serde_json::to_string(&hw.assets().bank_info(asset_type)).unwrap_or_default(); + ret.push_string(json); + Ok(()) + } + Syscall::BankSlotInfo => { + let asset_type = match expect_int(args, 0)? as u32 { + 0 => BankType::TILES, + 1 => BankType::SOUNDS, + _ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())), + }; + let slot = SlotRef { asset_type, index: expect_int(args, 1)? as usize }; + let json = serde_json::to_string(&hw.assets().slot_info(slot)).unwrap_or_default(); + ret.push_string(json); + Ok(()) + } + } + } +} + +fn push_button(ret: &mut HostReturn, button: &Button) -> Result<(), VmFault> { + ret.push_bool(button.pressed); + ret.push_bool(button.released); + ret.push_bool(button.down); + ret.push_int(button.hold_frames as i64); + Ok(()) +} + +fn expect_string(args: &[Value], index: usize, field: &str) -> Result { + match args.get(index).ok_or_else(|| VmFault::Panic(format!("Missing {}", field)))? { + Value::String(value) => Ok(value.clone()), + _ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected string {}", field))), + } +} + +fn expect_number(args: &[Value], index: usize, field: &str) -> Result { + match args.get(index).ok_or_else(|| VmFault::Panic(format!("Missing {}", field)))? { + Value::Float(f) => Ok(*f), + Value::Int32(i) => Ok(*i as f64), + Value::Int64(i) => Ok(*i as f64), + _ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected number for {}", field))), + } +} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs new file mode 100644 index 00000000..217c9b50 --- /dev/null +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs @@ -0,0 +1,145 @@ +use super::*; +use crate::CrashReport; +use crate::fs::FsBackend; +use prometeu_hal::cartridge::Cartridge; +use prometeu_hal::log::{LogLevel, LogSource}; + +impl VirtualMachineRuntime { + pub fn new(cap_config: Option) -> Self { + let boot_time = Instant::now(); + let mut os = Self { + tick_index: 0, + logical_frame_index: 0, + logical_frame_active: false, + logical_frame_remaining_cycles: 0, + last_frame_cpu_time_us: 0, + fs: VirtualFS::new(), + fs_state: FsState::Unmounted, + open_files: HashMap::new(), + next_handle: 1, + log_service: LogService::new(4096), + current_app_id: 0, + current_cartridge_title: String::new(), + current_cartridge_app_version: String::new(), + current_cartridge_app_mode: AppMode::Game, + current_entrypoint: String::new(), + logs_written_this_frame: HashMap::new(), + telemetry_current: TelemetryFrame::default(), + telemetry_last: TelemetryFrame::default(), + last_crash_report: None, + certifier: Certifier::new(cap_config.unwrap_or_default()), + paused: false, + debug_step_request: false, + needs_prepare_entry_call: false, + boot_time, + }; + + os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string()); + + os + } + + pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) { + let ts_ms = self.boot_time.elapsed().as_millis() as u64; + let frame = self.logical_frame_index; + self.log_service.log(ts_ms, frame, level, source, tag, msg); + } + + pub fn mount_fs(&mut self, backend: Box) { + self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string()); + match self.fs.mount(backend) { + Ok(_) => { + self.fs_state = FsState::Mounted; + self.log( + LogLevel::Info, + LogSource::Fs, + 0, + "Filesystem mounted successfully".to_string(), + ); + } + Err(e) => { + let err_msg = format!("Failed to mount filesystem: {:?}", e); + self.log(LogLevel::Error, LogSource::Fs, 0, err_msg); + self.fs_state = FsState::Error(e); + } + } + } + + pub fn unmount_fs(&mut self) { + self.fs.unmount(); + self.fs_state = FsState::Unmounted; + } + + pub(crate) fn update_fs(&mut self) { + if self.fs_state == FsState::Mounted && !self.fs.is_healthy() { + self.log( + LogLevel::Error, + LogSource::Fs, + 0, + "Filesystem became unhealthy, unmounting".to_string(), + ); + self.unmount_fs(); + } + } + + pub(crate) fn clear_cartridge_state(&mut self) { + self.logical_frame_index = 0; + self.logical_frame_active = false; + self.logical_frame_remaining_cycles = 0; + self.last_frame_cpu_time_us = 0; + + self.open_files.clear(); + self.next_handle = 1; + + self.current_app_id = 0; + self.current_cartridge_title.clear(); + self.current_cartridge_app_version.clear(); + self.current_cartridge_app_mode = AppMode::Game; + self.current_entrypoint.clear(); + self.logs_written_this_frame.clear(); + + self.telemetry_current = TelemetryFrame::default(); + self.telemetry_last = TelemetryFrame::default(); + self.last_crash_report = None; + + self.paused = false; + self.debug_step_request = false; + self.needs_prepare_entry_call = false; + } + + pub fn reset(&mut self, vm: &mut VirtualMachine) { + *vm = VirtualMachine::default(); + self.clear_cartridge_state(); + } + + pub fn initialize_vm( + &mut self, + vm: &mut VirtualMachine, + cartridge: &Cartridge, + ) -> Result<(), CrashReport> { + self.clear_cartridge_state(); + vm.set_capabilities(cartridge.capabilities); + + match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) { + Ok(_) => { + self.current_app_id = cartridge.app_id; + self.current_cartridge_title = cartridge.title.clone(); + self.current_cartridge_app_version = cartridge.app_version.clone(); + self.current_cartridge_app_mode = cartridge.app_mode; + self.current_entrypoint = cartridge.entrypoint.clone(); + Ok(()) + } + Err(e) => { + let report = CrashReport::VmInit { error: e }; + self.last_crash_report = Some(report.clone()); + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + format!("Failed to initialize VM: {}", report), + ); + Err(report) + } + } + } +} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs new file mode 100644 index 00000000..f3b0fdfc --- /dev/null +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -0,0 +1,277 @@ +use super::*; +use prometeu_bytecode::TRAP_TYPE; +use prometeu_bytecode::Value; +use prometeu_bytecode::assembler::assemble; +use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; +use prometeu_drivers::hardware::Hardware; +use prometeu_hal::InputSignals; +use prometeu_hal::cartridge::Cartridge; +use prometeu_hal::syscalls::caps; +use prometeu_vm::VmInitError; + +fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { + Cartridge { + app_id: 42, + title: "Test Cart".into(), + app_version: "1.0.0".into(), + app_mode: AppMode::Game, + entrypoint: "".into(), + capabilities, + program, + assets: vec![], + asset_table: vec![], + preload: vec![], + } +} + +fn serialized_single_function_module(code: Vec, syscalls: Vec) -> Vec { + BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![FunctionMeta { + code_offset: 0, + code_len: code.len() as u32, + ..Default::default() + }], + code, + debug_info: None, + exports: vec![], + syscalls, + } + .serialize() +} + +#[test] +fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::NONE); + + let res = runtime.initialize_vm(&mut vm, &cartridge); + + assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); + assert_eq!(runtime.current_app_id, 0); + assert_eq!(vm.pc(), 0); + assert_eq!(vm.operand_stack_top(1), Vec::::new()); +} + +#[test] +fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + let res = runtime.initialize_vm(&mut vm, &cartridge); + + assert!(res.is_ok()); + assert_eq!(runtime.current_app_id, 42); + assert!(!vm.is_halted()); +} + +#[test] +fn tick_returns_error_when_vm_ends_slice_with_trap() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + + let report = + runtime.tick(&mut vm, &signals, &mut hardware).expect("trap must surface as runtime error"); + + match report { + CrashReport::VmTrap { trap } => { + assert_eq!(trap.code, TRAP_TYPE); + assert!(trap.message.contains("Expected integer at index 0")); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + } + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); +} + +#[test] +fn tick_returns_panic_report_distinct_from_trap() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + let report = runtime + .tick(&mut vm, &signals, &mut hardware) + .expect("panic must surface as runtime error"); + + match report { + CrashReport::VmPanic { message, pc } => { + assert!(message.contains("HOSTCALL 0 reached execution without loader patching")); + assert!(pc.is_some()); + } + other => panic!("expected VmPanic crash report, got {:?}", other), + } + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. }))); +} + +#[test] +fn initialize_vm_success_clears_previous_crash_report() { + let mut runtime = VirtualMachineRuntime::new(None); + runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) }); + + let mut vm = VirtualMachine::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + assert!(runtime.last_crash_report.is_none()); +} + +#[test] +fn reset_clears_cartridge_scoped_runtime_state() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + + runtime.tick_index = 77; + runtime.logical_frame_index = 12; + runtime.logical_frame_active = true; + runtime.logical_frame_remaining_cycles = 345; + runtime.last_frame_cpu_time_us = 678; + runtime.fs_state = FsState::Mounted; + runtime.open_files.insert(7, "/save.dat".into()); + runtime.next_handle = 9; + runtime.current_app_id = 42; + runtime.current_cartridge_title = "Cart".into(); + runtime.current_cartridge_app_version = "1.2.3".into(); + runtime.current_cartridge_app_mode = AppMode::System; + runtime.current_entrypoint = "main".into(); + runtime.logs_written_this_frame.insert(42, 3); + runtime.telemetry_current.frame_index = 8; + runtime.telemetry_current.cycles_used = 99; + runtime.telemetry_last.frame_index = 7; + runtime.telemetry_last.completed_logical_frames = 2; + runtime.last_crash_report = + Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(55) }); + runtime.paused = true; + runtime.debug_step_request = true; + runtime.needs_prepare_entry_call = true; + + runtime.reset(&mut vm); + + assert_eq!(runtime.tick_index, 77); + assert_eq!(runtime.fs_state, FsState::Mounted); + assert_eq!(runtime.logical_frame_index, 0); + assert!(!runtime.logical_frame_active); + assert_eq!(runtime.logical_frame_remaining_cycles, 0); + assert_eq!(runtime.last_frame_cpu_time_us, 0); + assert!(runtime.open_files.is_empty()); + assert_eq!(runtime.next_handle, 1); + assert_eq!(runtime.current_app_id, 0); + assert!(runtime.current_cartridge_title.is_empty()); + assert!(runtime.current_cartridge_app_version.is_empty()); + assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); + assert!(runtime.current_entrypoint.is_empty()); + assert!(runtime.logs_written_this_frame.is_empty()); + assert_eq!(runtime.telemetry_current.frame_index, 0); + assert_eq!(runtime.telemetry_current.cycles_used, 0); + assert_eq!(runtime.telemetry_last.frame_index, 0); + assert_eq!(runtime.telemetry_last.completed_logical_frames, 0); + assert!(runtime.last_crash_report.is_none()); + assert!(!runtime.paused); + assert!(!runtime.debug_step_request); + assert!(!runtime.needs_prepare_entry_call); + assert_eq!(vm.pc(), 0); +} + +#[test] +fn initialize_vm_failure_clears_previous_identity_and_handles() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + + let good_program = serialized_single_function_module( + assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let good_cartridge = cartridge_with_program(good_program, caps::GFX); + runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize"); + + runtime.open_files.insert(5, "/save.dat".into()); + runtime.next_handle = 6; + runtime.paused = true; + runtime.debug_step_request = true; + runtime.telemetry_current.cycles_used = 123; + + let bad_program = serialized_single_function_module( + assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let bad_cartridge = cartridge_with_program(bad_program, caps::NONE); + + let res = runtime.initialize_vm(&mut vm, &bad_cartridge); + + assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); + assert_eq!(runtime.current_app_id, 0); + assert!(runtime.current_cartridge_title.is_empty()); + assert!(runtime.current_cartridge_app_version.is_empty()); + assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); + assert!(runtime.current_entrypoint.is_empty()); + assert!(runtime.open_files.is_empty()); + assert_eq!(runtime.next_handle, 1); + assert!(!runtime.paused); + assert!(!runtime.debug_step_request); + assert_eq!(runtime.telemetry_current.cycles_used, 0); + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmInit { .. }))); +} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs new file mode 100644 index 00000000..9d87c06f --- /dev/null +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs @@ -0,0 +1,203 @@ +use super::*; +use crate::CrashReport; +use prometeu_hal::asset::BankType; +use prometeu_hal::log::{LogLevel, LogSource}; +use prometeu_hal::{HardwareBridge, HostContext, InputSignals}; +use prometeu_vm::LogicalFrameEndingReason; + +impl VirtualMachineRuntime { + pub fn debug_step_instruction( + &mut self, + vm: &mut VirtualMachine, + hw: &mut dyn HardwareBridge, + ) -> Option { + let mut ctx = HostContext::new(Some(hw)); + match vm.step(self, &mut ctx) { + Ok(_) => None, + Err(e) => { + let report = match e { + LogicalFrameEndingReason::Trap(trap) => CrashReport::VmTrap { trap }, + LogicalFrameEndingReason::Panic(message) => { + CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) } + } + other => CrashReport::VmPanic { + message: format!("Unexpected fault during step: {:?}", other), + pc: Some(vm.pc() as u32), + }, + }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + format!("PVM Fault during Step: {}", report), + ); + self.last_crash_report = Some(report.clone()); + Some(report) + } + } + } + + pub fn tick( + &mut self, + vm: &mut VirtualMachine, + signals: &InputSignals, + hw: &mut dyn HardwareBridge, + ) -> Option { + let start = Instant::now(); + self.tick_index += 1; + + if self.paused && !self.debug_step_request { + return None; + } + + self.update_fs(); + + if !self.logical_frame_active { + self.logical_frame_active = true; + self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; + self.begin_logical_frame(signals, hw); + + if self.needs_prepare_entry_call || vm.call_stack_is_empty() { + vm.prepare_call(&self.current_entrypoint); + self.needs_prepare_entry_call = false; + } + + self.telemetry_current = TelemetryFrame { + frame_index: self.logical_frame_index, + cycles_budget: self + .certifier + .config + .cycles_budget_per_frame + .unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME), + ..Default::default() + }; + } + + let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles); + + if budget > 0 { + let run_result = { + let mut ctx = HostContext::new(Some(hw)); + vm.run_budget(budget, self, &mut ctx) + }; + + match run_result { + Ok(run) => { + self.logical_frame_remaining_cycles = + self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used); + self.telemetry_current.cycles_used += run.cycles_used; + self.telemetry_current.vm_steps += run.steps_executed; + + if run.reason == LogicalFrameEndingReason::Breakpoint { + self.paused = true; + self.debug_step_request = false; + self.log( + LogLevel::Info, + LogSource::Vm, + 0xDEB1, + format!("Breakpoint hit at PC 0x{:X}", vm.pc()), + ); + } + + if let LogicalFrameEndingReason::Panic(err) = run.reason { + let report = + CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + report.summary(), + ); + self.last_crash_report = Some(report.clone()); + return Some(report); + } + + if let LogicalFrameEndingReason::Trap(trap) = &run.reason { + let report = CrashReport::VmTrap { trap: trap.clone() }; + self.log( + LogLevel::Error, + LogSource::Vm, + report.log_tag(), + report.summary(), + ); + self.last_crash_report = Some(report.clone()); + return Some(report); + } + + if run.reason == LogicalFrameEndingReason::FrameSync + || run.reason == LogicalFrameEndingReason::EndOfRom + { + hw.gfx_mut().render_all(); + self.telemetry_current.host_cpu_time_us = + start.elapsed().as_micros() as u64; + + let ts_ms = self.boot_time.elapsed().as_millis() as u64; + self.telemetry_current.violations = self.certifier.evaluate( + &self.telemetry_current, + &mut self.log_service, + ts_ms, + ) as u32; + + self.telemetry_current.completed_logical_frames += 1; + self.telemetry_last = self.telemetry_current; + + self.logical_frame_index += 1; + self.logical_frame_active = false; + self.logical_frame_remaining_cycles = 0; + + if run.reason == LogicalFrameEndingReason::FrameSync { + self.needs_prepare_entry_call = true; + } + + if self.debug_step_request { + self.paused = true; + self.debug_step_request = false; + } + } + } + Err(e) => { + let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) }; + self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary()); + self.last_crash_report = Some(report.clone()); + return Some(report); + } + } + } + + self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; + + let gfx_stats = hw.assets().bank_info(BankType::TILES); + self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes; + self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes; + self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32; + + let audio_stats = hw.assets().bank_info(BankType::SOUNDS); + self.telemetry_current.audio_used_bytes = audio_stats.used_bytes; + self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes; + self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32; + + if !self.logical_frame_active + && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) + { + self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us; + self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget; + self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes; + self.telemetry_last.gfx_inflight_bytes = self.telemetry_current.gfx_inflight_bytes; + self.telemetry_last.gfx_slots_occupied = self.telemetry_current.gfx_slots_occupied; + self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes; + self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes; + self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied; + } + + None + } + + pub(crate) fn begin_logical_frame( + &mut self, + _signals: &InputSignals, + hw: &mut dyn HardwareBridge, + ) { + hw.audio_mut().clear_commands(); + self.logs_written_this_frame.clear(); + } +} diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index e1ee3412..5c0402b8 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -1,3 +1,8 @@ +mod gc; +mod loader; +mod runtime; +mod stack; + use crate::call_frame::CallFrame; use crate::heap::{CoroutineState, Heap}; use crate::lookup_intrinsic_by_id; @@ -20,73 +25,6 @@ use prometeu_bytecode::{ use prometeu_hal::syscalls::caps::NONE; use prometeu_hal::vm_fault::VmFault; -fn patch_module_hostcalls( - module: &mut BytecodeModule, - capabilities: prometeu_hal::syscalls::CapFlags, -) -> Result<(), LoaderPatchError> { - let resolved = - prometeu_hal::syscalls::resolve_declared_program_syscalls(&module.syscalls, capabilities) - .map_err(LoaderPatchError::ResolveFailed)?; - - let mut used = vec![false; module.syscalls.len()]; - let mut pc = 0usize; - while pc < module.code.len() { - let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?; - let next_pc = instr.next_pc; - - match instr.opcode { - OpCode::Hostcall => { - let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?; - let Some(resolved_syscall) = resolved.get(sysc_index as usize) else { - return Err(LoaderPatchError::HostcallIndexOutOfBounds { - pc, - sysc_index, - syscalls_len: resolved.len(), - }); - }; - - used[sysc_index as usize] = true; - module.code[pc..pc + 2].copy_from_slice(&(OpCode::Syscall as u16).to_le_bytes()); - module.code[pc + 2..pc + 6].copy_from_slice(&resolved_syscall.id.to_le_bytes()); - } - OpCode::Syscall => { - return Err(LoaderPatchError::RawSyscallInPreloadArtifact { - pc, - syscall_id: instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?, - }); - } - _ => {} - } - - pc = next_pc; - } - - for (index, (was_used, decl)) in used.iter().zip(&module.syscalls).enumerate() { - if !was_used { - return Err(LoaderPatchError::UnusedSyscallDecl { - sysc_index: index as u32, - module: decl.module.clone(), - name: decl.name.clone(), - version: decl.version, - }); - } - } - - let mut pc = 0usize; - while pc < module.code.len() { - let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?; - if instr.opcode == OpCode::Hostcall { - return Err(LoaderPatchError::HostcallRemaining { - pc, - sysc_index: instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?, - }); - } - pc = instr.next_pc; - } - - Ok(()) -} - /// Reason why the Virtual Machine stopped execution during a specific run. /// This allows the system to decide if it should continue execution in the next tick /// or if the frame is finalized. @@ -256,310 +194,6 @@ impl VirtualMachine { } } - /// Resets the VM state and loads a new program. - /// This is typically called by the Firmware when starting a new App/Cartridge. - pub fn initialize( - &mut self, - program_bytes: Vec, - entrypoint: &str, - ) -> Result<(), VmInitError> { - // Fail fast: reset state upfront. If we return early with an error, - // the VM is left in a "halted and empty" state. - self.program = ProgramImage::default(); - self.pc = 0; - self.operand_stack.clear(); - self.call_stack.clear(); - self.globals.clear(); - self.heap = Heap::new(); - self.cycles = 0; - self.halted = true; // execution is impossible until a successful load - self.last_gc_live_count = 0; - self.current_tick = 0; - self.sleep_requested_until = None; - self.scheduler = Scheduler::new(); - self.current_coro = None; - // 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") { - match prometeu_bytecode::BytecodeLoader::load(&program_bytes) { - Ok(mut module) => { - patch_module_hostcalls(&mut module, self.capabilities) - .map_err(VmInitError::LoaderPatchFailed)?; - - // Run verifier on the module - let max_stacks = Verifier::verify(&module.code, &module.functions) - .map_err(|e| VmInitError::VerificationFailed(format!("{:?}", e)))?; - - let mut program = ProgramImage::from(module); - - let mut functions = program.functions.as_ref().to_vec(); - for (func, max_stack) in functions.iter_mut().zip(max_stacks) { - func.max_stack_slots = max_stack; - } - program.functions = std::sync::Arc::from(functions); - - program - } - Err(prometeu_bytecode::LoadError::InvalidVersion) => { - return Err(VmInitError::UnsupportedFormat); - } - Err(e) => { - return Err(VmInitError::ImageLoadFailed(e)); - } - } - } else { - return Err(VmInitError::InvalidFormat); - }; - - // Resolve the entrypoint: empty (defaults to func 0), numeric func_idx, or symbol name. - let pc = if entrypoint.is_empty() { - program.functions.first().map(|f| f.code_offset as usize).unwrap_or(0) - } else if let Ok(func_idx) = entrypoint.parse::() { - program - .functions - .get(func_idx) - .map(|f| f.code_offset as usize) - .ok_or(VmInitError::EntrypointNotFound)? - } else { - // Try to resolve as a symbol name from the exports map - if let Some(&func_idx) = program.exports.get(entrypoint) { - program - .functions - .get(func_idx as usize) - .map(|f| f.code_offset as usize) - .ok_or(VmInitError::EntrypointNotFound)? - } else { - return Err(VmInitError::EntrypointNotFound); - } - }; - - // Finalize initialization by applying the new program and PC. - self.program = program; - self.pc = pc; - self.halted = false; // Successfully loaded, execution is now possible - - 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) { - let func_idx = if let Ok(idx) = entrypoint.parse::() { - idx - } else { - // Try to resolve as a symbol name - self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) - // Default to 0 if not found - }; - - let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default(); - let addr = callee.code_offset as usize; - - self.pc = addr; - self.halted = false; - - // Pushing a sentinel frame so RET works at the top level. - // The return address is set to the end of ROM, which will naturally - // cause the VM to stop after returning from the entrypoint. - self.operand_stack.clear(); - self.call_stack.clear(); - - // Entrypoint also needs locals allocated. - // For the sentinel frame, stack_base is always 0. - if let Some(func) = self.program.functions.get(func_idx) { - let total_slots = func.param_slots as u32 + func.local_slots as u32; - for _ in 0..total_slots { - self.operand_stack.push(Value::Null); - } - } - - self.call_stack.push(CallFrame { - return_pc: self.program.rom.len() as u32, - stack_base: 0, - func_idx, - }); - - // Initialize the main coroutine object. - // IMPORTANT INVARIANT: - // - The RUNNING coroutine's authoritative execution state lives in the VM fields - // (pc, operand_stack, call_stack). - // - The heap-side coroutine object is authoritative ONLY when the coroutine is suspended - // (Ready/Sleeping/Finished). While running, its `stack`/`frames` should be empty. - // - // Therefore we do NOT clone the VM stacks into the heap here. We create the main - // coroutine object with empty stack/frames and mark it as Running, and the VM already - // holds the live execution context initialized above. - let main_href = self.heap.allocate_coroutine( - self.pc, - CoroutineState::Running, - 0, - Vec::new(), - Vec::new(), - ); - self.current_coro = Some(main_href); - self.scheduler.set_current(self.current_coro); - } - - /// Executes the VM for a limited number of cycles (budget). - /// - /// This is the heart of the deterministic execution model. Instead of running - /// indefinitely, the VM runs until it consumes its allocated budget or reaches - /// a synchronization point (`FRAME_SYNC`). - /// - /// # Arguments - /// * `budget` - Maximum number of cycles allowed for this execution slice. - /// * `native` - Interface for handling syscalls (Firmware/OS). - /// * `hw` - Access to virtual hardware peripherals. - pub fn run_budget( - &mut self, - budget: u64, - native: &mut dyn NativeInterface, - ctx: &mut HostContext, - ) -> Result { - let start_cycles = self.cycles; - let mut steps_executed = 0; - let mut ending_reason: Option = None; - - while (self.cycles - start_cycles) < budget - && !self.halted - && self.pc < self.program.rom.len() - { - // Debugger support: stop before executing an instruction if there's a breakpoint. - // Note: we skip the check for the very first step of a slice to avoid - // getting stuck on the same breakpoint repeatedly. - if steps_executed > 0 && self.breakpoints.contains(&self.pc) { - ending_reason = Some(LogicalFrameEndingReason::Breakpoint); - break; - } - - let pc_before = self.pc; - let cycles_before = self.cycles; - - // Execute a single step (Fetch-Decode-Execute) - if let Err(reason) = self.step(native, ctx) { - ending_reason = Some(reason); - break; - } - steps_executed += 1; - - // Integrity check: ensure real progress is being made to avoid infinite loops - // caused by zero-cycle instructions or stuck PC. - if self.pc == pc_before && self.cycles == cycles_before && !self.halted { - ending_reason = Some(LogicalFrameEndingReason::Panic(format!( - "VM stuck at PC 0x{:08X}", - self.pc - ))); - break; - } - } - - // Determine why we stopped if no explicit reason (FrameSync/Breakpoint) was set. - if ending_reason.is_none() { - if self.halted { - ending_reason = Some(LogicalFrameEndingReason::Halted); - } else if self.pc >= self.program.rom.len() { - ending_reason = Some(LogicalFrameEndingReason::EndOfRom); - } else { - ending_reason = Some(LogicalFrameEndingReason::BudgetExhausted); - } - } - - Ok(BudgetReport { - cycles_used: self.cycles - start_cycles, - steps_executed, - reason: ending_reason.unwrap(), - }) - } - - /// Harness: run exactly `frames` logical frames deterministically. - /// - /// This repeatedly calls `run_budget` with the provided `budget_per_slice` until - /// a full logical frame is completed (i.e., a `FrameSync` is observed). If a - /// terminal condition is reached earlier (Halt/EndOfRom/Panic/Trap/Breakpoint), - /// the function returns early with all collected slice reports so far. - pub fn run_frames( - &mut self, - frames: u64, - budget_per_slice: u64, - native: &mut dyn NativeInterface, - ctx: &mut HostContext, - ) -> Result, String> { - assert!(budget_per_slice > 0, "budget_per_slice must be > 0"); - - let mut out = Vec::new(); - let mut frames_done = 0u64; - while frames_done < frames { - let rep = self.run_budget(budget_per_slice, native, ctx)?; - let terminal = matches!( - rep.reason, - LogicalFrameEndingReason::Halted - | LogicalFrameEndingReason::EndOfRom - | LogicalFrameEndingReason::Panic(_) - | LogicalFrameEndingReason::Trap(_) - | LogicalFrameEndingReason::Breakpoint - ); - - let is_frame_end = matches!(rep.reason, LogicalFrameEndingReason::FrameSync); - out.push(rep); - - if terminal { - break; - } - if is_frame_end { - frames_done += 1; - } - } - Ok(out) - } - - /// Harness: alias for `run_frames(frames, ...)`. - pub fn run_ticks( - &mut self, - ticks: u64, - budget_per_slice: u64, - native: &mut dyn NativeInterface, - ctx: &mut HostContext, - ) -> Result, String> { - self.run_frames(ticks, budget_per_slice, native, ctx) - } - - /// Harness: run until HALT/EndOfRom/Panic/Trap/Breakpoint deterministically. - /// - /// Repeatedly invokes `run_budget` with a fixed `budget_per_slice`, collecting - /// each slice's report until a terminal condition is reached. - pub fn run_until_halt( - &mut self, - budget_per_slice: u64, - native: &mut dyn NativeInterface, - ctx: &mut HostContext, - ) -> Result, String> { - assert!(budget_per_slice > 0, "budget_per_slice must be > 0"); - - let mut out = Vec::new(); - loop { - let rep = self.run_budget(budget_per_slice, native, ctx)?; - let terminal = matches!( - rep.reason, - LogicalFrameEndingReason::Halted - | LogicalFrameEndingReason::EndOfRom - | LogicalFrameEndingReason::Panic(_) - | LogicalFrameEndingReason::Trap(_) - | LogicalFrameEndingReason::Breakpoint - ); - out.push(rep); - if terminal { - break; - } - } - Ok(out) - } - /// Executes a single instruction at the current Program Counter (PC). /// /// This follows the classic CPU cycle: @@ -1535,249 +1169,6 @@ impl VirtualMachine { self.cycles += opcode.cycles(); Ok(()) } - - /// Perform safepoint duties that occur at logical frame boundaries. - /// Runs GC if thresholds are reached, clears cooperative yield flag, - /// and advances the logical tick counter. - fn handle_safepoint(&mut self) { - // 1) GC Safepoint: only at FRAME_SYNC-like boundaries - if self.gc_alloc_threshold > 0 { - let live_now = self.heap.len(); - let since_last = live_now.saturating_sub(self.last_gc_live_count); - if since_last >= self.gc_alloc_threshold { - // Collect GC roots from VM state - struct CollectRoots(Vec); - impl crate::roots::RootVisitor for CollectRoots { - fn visit_heap_ref(&mut self, r: prometeu_bytecode::HeapRef) { - self.0.push(r); - } - } - let mut collector = CollectRoots(Vec::new()); - self.visit_roots(&mut collector); - // Add current coroutine and all suspended (ready/sleeping) coroutines as GC roots - if let Some(cur) = self.current_coro { - collector.0.push(cur); - } - let mut coro_roots = self.heap.suspended_coroutine_handles(); - collector.0.append(&mut coro_roots); - - // Run mark-sweep - self.heap.mark_from_roots(collector.0); - self.heap.sweep(); - // Update baseline for next cycles - self.last_gc_live_count = self.heap.len(); - } - } - - // 2) Advance logical tick and wake sleepers - self.current_tick = self.current_tick.wrapping_add(1); - self.scheduler.wake_ready(self.current_tick); - - // 3) Apply pending transitions for the current coroutine (yield/sleep/finished) - let mut switched_out = false; - if let Some(cur) = self.current_coro { - // Handle sleep request - if let Some(wake) = self.sleep_requested_until.take() { - if let Some(co) = self.heap.coroutine_data_mut(cur) { - // Save execution context into the coroutine object - co.pc = self.pc; - co.stack = std::mem::take(&mut self.operand_stack); - co.frames = std::mem::take(&mut self.call_stack); - co.state = CoroutineState::Sleeping; - co.wake_tick = wake; - } - self.scheduler.sleep_until(cur, wake); - self.current_coro = None; - self.scheduler.clear_current(); - switched_out = true; - } else if self.yield_requested { - if let Some(co) = self.heap.coroutine_data_mut(cur) { - co.pc = self.pc; - co.stack = std::mem::take(&mut self.operand_stack); - co.frames = std::mem::take(&mut self.call_stack); - co.state = CoroutineState::Ready; - } - self.scheduler.enqueue_ready(cur); - self.current_coro = None; - self.scheduler.clear_current(); - switched_out = true; - } else if self.halted || self.pc >= self.program.rom.len() { - // Current finished; save final context and mark Finished - if let Some(co) = self.heap.coroutine_data_mut(cur) { - co.pc = self.pc; - co.stack = std::mem::take(&mut self.operand_stack); - co.frames = std::mem::take(&mut self.call_stack); - co.state = CoroutineState::Finished; - } - self.current_coro = None; - self.scheduler.clear_current(); - switched_out = true; - } else { - // Stays running; nothing to do - } - } - - // 4) Select next coroutine if needed - if self.current_coro.is_none() { - if let Some(next) = self.scheduler.dequeue_next() { - // Load next context into the VM - if let Some(co) = self.heap.coroutine_data_mut(next) { - self.pc = co.pc; - self.operand_stack = std::mem::take(&mut co.stack); - self.call_stack = std::mem::take(&mut co.frames); - co.state = CoroutineState::Running; - } - self.current_coro = Some(next); - self.scheduler.set_current(self.current_coro); - } else { - // Nothing ready now. If there are sleeping coroutines, we keep VM idle until next frame tick. - // If there are no sleeping coroutines either (i.e., all finished), we can halt deterministically. - if switched_out && !self.scheduler.has_sleeping() { - self.halted = true; - } - } - } else { - // Keep current as scheduler current for observability - self.scheduler.set_current(self.current_coro); - } - - // 5) Clear cooperative yield request at the safepoint boundary. - self.yield_requested = false; - } - - // /// Save the currently running VM execution context back into its coroutine object. - // /// Must be called only at safepoints. - // fn save_current_context_into_coroutine(&mut self) { - // if let Some(cur) = self.current_coro { - // if let Some(co) = self.heap.coroutine_data_mut(cur) { - // co.pc = self.pc; - // co.stack = std::mem::take(&mut self.operand_stack); - // co.frames = std::mem::take(&mut self.call_stack); - // } - // } - // } - - // /// Load a coroutine context from heap into the VM runtime state. - // /// Must be called only at safepoints. - // fn load_coroutine_context_into_vm(&mut self, coro: HeapRef) { - // if let Some(co) = self.heap.coroutine_data_mut(coro) { - // self.pc = co.pc; - // self.operand_stack = std::mem::take(&mut co.stack); - // self.call_stack = std::mem::take(&mut co.frames); - // co.state = CoroutineState::Running; - // } - // self.current_coro = Some(coro); - // self.scheduler.set_current(self.current_coro); - // } - - pub fn trap( - &self, - code: u32, - opcode: u16, - message: String, - pc: u32, - ) -> LogicalFrameEndingReason { - LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc)) - } - - pub fn push(&mut self, val: Value) { - self.operand_stack.push(val); - } - - pub fn pop(&mut self) -> Result { - self.operand_stack.pop().ok_or("Stack underflow".into()) - } - - pub fn pop_number(&mut self) -> Result { - let val = self.pop()?; - val.as_float().ok_or_else(|| "Expected number".into()) - } - - pub fn pop_integer(&mut self) -> Result { - let val = self.pop()?; - if let Value::Boolean(b) = val { - return Ok(if b { 1 } else { 0 }); - } - val.as_integer().ok_or_else(|| "Expected integer".into()) - } - - pub fn peek(&self) -> Result<&Value, String> { - self.operand_stack.last().ok_or("Stack underflow".into()) - } - - fn pop_trap>( - &mut self, - opcode: OpCode, - pc: u32, - message: S, - ) -> Result { - self.pop().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc)) - } - - fn peek_trap>( - &self, - opcode: OpCode, - pc: u32, - message: S, - ) -> Result<&Value, LogicalFrameEndingReason> { - self.peek().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc)) - } - - fn binary_op( - &mut self, - opcode: OpCode, - start_pc: u32, - f: F, - ) -> Result<(), LogicalFrameEndingReason> - where - F: FnOnce(Value, Value) -> Result, - { - let b = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?; - let a = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?; - match f(a, b) { - Ok(res) => { - self.push(res); - Ok(()) - } - Err(OpError::Trap(code, msg)) => Err(self.trap(code, opcode as u16, msg, start_pc)), - } - } - - /// Visit all GC roots reachable from the VM state. - /// This includes: - /// - Entire operand stack values - /// - Locals/args in each call frame (derived from `stack_base` and function layout) - /// - Global variables - pub fn visit_roots(&self, visitor: &mut V) { - // 1) Operand stack (all values are roots) - for v in &self.operand_stack { - visit_value_for_roots(v, visitor); - } - - // 2) Call frames: iterate locals/args range for each frame - for frame in &self.call_stack { - if let Some(func_meta) = self.program.functions.get(frame.func_idx) { - let start = frame.stack_base; - let frame_slots = - (func_meta.param_slots as usize) + (func_meta.local_slots as usize); - let mut end = start.saturating_add(frame_slots); - // Clamp to current stack height just in case - if end > self.operand_stack.len() { - end = self.operand_stack.len(); - } - for i in start..end { - if let Some(v) = self.operand_stack.get(i) { - visit_value_for_roots(v, visitor); - } - } - } - } - - // 3) Globals - for g in &self.globals { - visit_value_for_roots(g, visitor); - } - } } #[cfg(test)] diff --git a/crates/console/prometeu-vm/src/virtual_machine/gc.rs b/crates/console/prometeu-vm/src/virtual_machine/gc.rs new file mode 100644 index 00000000..53b5badd --- /dev/null +++ b/crates/console/prometeu-vm/src/virtual_machine/gc.rs @@ -0,0 +1,115 @@ +use super::*; + +impl VirtualMachine { + pub(super) fn handle_safepoint(&mut self) { + if self.gc_alloc_threshold > 0 { + let live_now = self.heap.len(); + let since_last = live_now.saturating_sub(self.last_gc_live_count); + if since_last >= self.gc_alloc_threshold { + struct CollectRoots(Vec); + impl crate::roots::RootVisitor for CollectRoots { + fn visit_heap_ref(&mut self, r: prometeu_bytecode::HeapRef) { + self.0.push(r); + } + } + let mut collector = CollectRoots(Vec::new()); + self.visit_roots(&mut collector); + if let Some(cur) = self.current_coro { + collector.0.push(cur); + } + let mut coro_roots = self.heap.suspended_coroutine_handles(); + collector.0.append(&mut coro_roots); + self.heap.mark_from_roots(collector.0); + self.heap.sweep(); + self.last_gc_live_count = self.heap.len(); + } + } + + self.current_tick = self.current_tick.wrapping_add(1); + self.scheduler.wake_ready(self.current_tick); + + let mut switched_out = false; + if let Some(cur) = self.current_coro { + if let Some(wake) = self.sleep_requested_until.take() { + if let Some(co) = self.heap.coroutine_data_mut(cur) { + co.pc = self.pc; + co.stack = std::mem::take(&mut self.operand_stack); + co.frames = std::mem::take(&mut self.call_stack); + co.state = CoroutineState::Sleeping; + co.wake_tick = wake; + } + self.scheduler.sleep_until(cur, wake); + self.current_coro = None; + self.scheduler.clear_current(); + switched_out = true; + } else if self.yield_requested { + if let Some(co) = self.heap.coroutine_data_mut(cur) { + co.pc = self.pc; + co.stack = std::mem::take(&mut self.operand_stack); + co.frames = std::mem::take(&mut self.call_stack); + co.state = CoroutineState::Ready; + } + self.scheduler.enqueue_ready(cur); + self.current_coro = None; + self.scheduler.clear_current(); + switched_out = true; + } else if self.halted || self.pc >= self.program.rom.len() { + if let Some(co) = self.heap.coroutine_data_mut(cur) { + co.pc = self.pc; + co.stack = std::mem::take(&mut self.operand_stack); + co.frames = std::mem::take(&mut self.call_stack); + co.state = CoroutineState::Finished; + } + self.current_coro = None; + self.scheduler.clear_current(); + switched_out = true; + } + } + + if self.current_coro.is_none() { + if let Some(next) = self.scheduler.dequeue_next() { + if let Some(co) = self.heap.coroutine_data_mut(next) { + self.pc = co.pc; + self.operand_stack = std::mem::take(&mut co.stack); + self.call_stack = std::mem::take(&mut co.frames); + co.state = CoroutineState::Running; + } + self.current_coro = Some(next); + self.scheduler.set_current(self.current_coro); + } else if switched_out && !self.scheduler.has_sleeping() { + self.halted = true; + } + } else { + self.scheduler.set_current(self.current_coro); + } + + self.yield_requested = false; + } + + pub fn visit_roots(&self, visitor: &mut V) { + for value in &self.operand_stack { + visit_value_for_roots(value, visitor); + } + + for frame in &self.call_stack { + if let Some(func_meta) = self.program.functions.get(frame.func_idx) { + let start = frame.stack_base; + let frame_slots = + (func_meta.param_slots as usize) + (func_meta.local_slots as usize); + let mut end = start.saturating_add(frame_slots); + if end > self.operand_stack.len() { + end = self.operand_stack.len(); + } + for index in start..end { + if let Some(value) = self.operand_stack.get(index) { + visit_value_for_roots(value, visitor); + } + } + } + } + + for global in &self.globals { + visit_value_for_roots(global, visitor); + } + } +} diff --git a/crates/console/prometeu-vm/src/virtual_machine/loader.rs b/crates/console/prometeu-vm/src/virtual_machine/loader.rs new file mode 100644 index 00000000..10f7047b --- /dev/null +++ b/crates/console/prometeu-vm/src/virtual_machine/loader.rs @@ -0,0 +1,182 @@ +use super::*; + +fn patch_module_hostcalls( + module: &mut BytecodeModule, + capabilities: prometeu_hal::syscalls::CapFlags, +) -> Result<(), LoaderPatchError> { + let resolved = + prometeu_hal::syscalls::resolve_declared_program_syscalls(&module.syscalls, capabilities) + .map_err(LoaderPatchError::ResolveFailed)?; + + let mut used = vec![false; module.syscalls.len()]; + let mut pc = 0usize; + while pc < module.code.len() { + let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?; + let next_pc = instr.next_pc; + + match instr.opcode { + OpCode::Hostcall => { + let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?; + let Some(resolved_syscall) = resolved.get(sysc_index as usize) else { + return Err(LoaderPatchError::HostcallIndexOutOfBounds { + pc, + sysc_index, + syscalls_len: resolved.len(), + }); + }; + + used[sysc_index as usize] = true; + module.code[pc..pc + 2].copy_from_slice(&(OpCode::Syscall as u16).to_le_bytes()); + module.code[pc + 2..pc + 6].copy_from_slice(&resolved_syscall.id.to_le_bytes()); + } + OpCode::Syscall => { + return Err(LoaderPatchError::RawSyscallInPreloadArtifact { + pc, + syscall_id: instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?, + }); + } + _ => {} + } + + pc = next_pc; + } + + for (index, (was_used, decl)) in used.iter().zip(&module.syscalls).enumerate() { + if !was_used { + return Err(LoaderPatchError::UnusedSyscallDecl { + sysc_index: index as u32, + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + }); + } + } + + let mut pc = 0usize; + while pc < module.code.len() { + let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?; + if instr.opcode == OpCode::Hostcall { + return Err(LoaderPatchError::HostcallRemaining { + pc, + sysc_index: instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?, + }); + } + pc = instr.next_pc; + } + + Ok(()) +} + +impl VirtualMachine { + pub fn initialize( + &mut self, + program_bytes: Vec, + entrypoint: &str, + ) -> Result<(), VmInitError> { + self.program = ProgramImage::default(); + self.pc = 0; + self.operand_stack.clear(); + self.call_stack.clear(); + self.globals.clear(); + self.heap = Heap::new(); + self.cycles = 0; + self.halted = true; + self.last_gc_live_count = 0; + self.current_tick = 0; + self.sleep_requested_until = None; + self.scheduler = Scheduler::new(); + self.current_coro = None; + + let program = if program_bytes.starts_with(b"PBS\0") { + match prometeu_bytecode::BytecodeLoader::load(&program_bytes) { + Ok(mut module) => { + patch_module_hostcalls(&mut module, self.capabilities) + .map_err(VmInitError::LoaderPatchFailed)?; + + let max_stacks = Verifier::verify(&module.code, &module.functions) + .map_err(|e| VmInitError::VerificationFailed(format!("{:?}", e)))?; + + let mut program = ProgramImage::from(module); + let mut functions = program.functions.as_ref().to_vec(); + for (func, max_stack) in functions.iter_mut().zip(max_stacks) { + func.max_stack_slots = max_stack; + } + program.functions = std::sync::Arc::from(functions); + program + } + Err(prometeu_bytecode::LoadError::InvalidVersion) => { + return Err(VmInitError::UnsupportedFormat); + } + Err(e) => return Err(VmInitError::ImageLoadFailed(e)), + } + } else { + return Err(VmInitError::InvalidFormat); + }; + + let pc = if entrypoint.is_empty() { + program.functions.first().map(|f| f.code_offset as usize).unwrap_or(0) + } else if let Ok(func_idx) = entrypoint.parse::() { + program + .functions + .get(func_idx) + .map(|f| f.code_offset as usize) + .ok_or(VmInitError::EntrypointNotFound)? + } else if let Some(&func_idx) = program.exports.get(entrypoint) { + program + .functions + .get(func_idx as usize) + .map(|f| f.code_offset as usize) + .ok_or(VmInitError::EntrypointNotFound)? + } else { + return Err(VmInitError::EntrypointNotFound); + }; + + self.program = program; + self.pc = pc; + self.halted = false; + + Ok(()) + } + + pub fn set_capabilities(&mut self, caps: prometeu_hal::syscalls::CapFlags) { + self.capabilities = caps; + } + + pub fn prepare_call(&mut self, entrypoint: &str) { + let func_idx = if let Ok(idx) = entrypoint.parse::() { + idx + } else { + self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) + }; + + let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default(); + self.pc = callee.code_offset as usize; + self.halted = false; + + self.operand_stack.clear(); + self.call_stack.clear(); + + if let Some(func) = self.program.functions.get(func_idx) { + let total_slots = func.param_slots as u32 + func.local_slots as u32; + for _ in 0..total_slots { + self.operand_stack.push(Value::Null); + } + } + + self.call_stack.push(CallFrame { + return_pc: self.program.rom.len() as u32, + stack_base: 0, + func_idx, + }); + + let main_href = self.heap.allocate_coroutine( + self.pc, + CoroutineState::Running, + 0, + Vec::new(), + Vec::new(), + ); + self.current_coro = Some(main_href); + self.scheduler.set_current(self.current_coro); + } +} diff --git a/crates/console/prometeu-vm/src/virtual_machine/runtime.rs b/crates/console/prometeu-vm/src/virtual_machine/runtime.rs new file mode 100644 index 00000000..7edef357 --- /dev/null +++ b/crates/console/prometeu-vm/src/virtual_machine/runtime.rs @@ -0,0 +1,129 @@ +use super::*; + +impl VirtualMachine { + pub fn run_budget( + &mut self, + budget: u64, + native: &mut dyn NativeInterface, + ctx: &mut HostContext, + ) -> Result { + let start_cycles = self.cycles; + let mut steps_executed = 0; + let mut ending_reason: Option = None; + + while (self.cycles - start_cycles) < budget + && !self.halted + && self.pc < self.program.rom.len() + { + if steps_executed > 0 && self.breakpoints.contains(&self.pc) { + ending_reason = Some(LogicalFrameEndingReason::Breakpoint); + break; + } + + let pc_before = self.pc; + let cycles_before = self.cycles; + + if let Err(reason) = self.step(native, ctx) { + ending_reason = Some(reason); + break; + } + steps_executed += 1; + + if self.pc == pc_before && self.cycles == cycles_before && !self.halted { + ending_reason = Some(LogicalFrameEndingReason::Panic(format!( + "VM stuck at PC 0x{:08X}", + self.pc + ))); + break; + } + } + + if ending_reason.is_none() { + ending_reason = Some(if self.halted { + LogicalFrameEndingReason::Halted + } else if self.pc >= self.program.rom.len() { + LogicalFrameEndingReason::EndOfRom + } else { + LogicalFrameEndingReason::BudgetExhausted + }); + } + + Ok(BudgetReport { + cycles_used: self.cycles - start_cycles, + steps_executed, + reason: ending_reason.unwrap(), + }) + } + + pub fn run_frames( + &mut self, + frames: u64, + budget_per_slice: u64, + native: &mut dyn NativeInterface, + ctx: &mut HostContext, + ) -> Result, String> { + assert!(budget_per_slice > 0, "budget_per_slice must be > 0"); + + let mut out = Vec::new(); + let mut frames_done = 0u64; + while frames_done < frames { + let rep = self.run_budget(budget_per_slice, native, ctx)?; + let terminal = matches!( + rep.reason, + LogicalFrameEndingReason::Halted + | LogicalFrameEndingReason::EndOfRom + | LogicalFrameEndingReason::Panic(_) + | LogicalFrameEndingReason::Trap(_) + | LogicalFrameEndingReason::Breakpoint + ); + + let is_frame_end = matches!(rep.reason, LogicalFrameEndingReason::FrameSync); + out.push(rep); + + if terminal { + break; + } + if is_frame_end { + frames_done += 1; + } + } + Ok(out) + } + + pub fn run_ticks( + &mut self, + ticks: u64, + budget_per_slice: u64, + native: &mut dyn NativeInterface, + ctx: &mut HostContext, + ) -> Result, String> { + self.run_frames(ticks, budget_per_slice, native, ctx) + } + + pub fn run_until_halt( + &mut self, + budget_per_slice: u64, + native: &mut dyn NativeInterface, + ctx: &mut HostContext, + ) -> Result, String> { + assert!(budget_per_slice > 0, "budget_per_slice must be > 0"); + + let mut out = Vec::new(); + loop { + let rep = self.run_budget(budget_per_slice, native, ctx)?; + let terminal = matches!( + rep.reason, + LogicalFrameEndingReason::Halted + | LogicalFrameEndingReason::EndOfRom + | LogicalFrameEndingReason::Panic(_) + | LogicalFrameEndingReason::Trap(_) + | LogicalFrameEndingReason::Breakpoint + ); + out.push(rep); + if terminal { + break; + } + } + Ok(out) + } +} diff --git a/crates/console/prometeu-vm/src/virtual_machine/stack.rs b/crates/console/prometeu-vm/src/virtual_machine/stack.rs new file mode 100644 index 00000000..2a098d69 --- /dev/null +++ b/crates/console/prometeu-vm/src/virtual_machine/stack.rs @@ -0,0 +1,76 @@ +use super::*; + +impl VirtualMachine { + pub fn trap( + &self, + code: u32, + opcode: u16, + message: String, + pc: u32, + ) -> LogicalFrameEndingReason { + LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc)) + } + + pub fn push(&mut self, val: Value) { + self.operand_stack.push(val); + } + + pub fn pop(&mut self) -> Result { + self.operand_stack.pop().ok_or("Stack underflow".into()) + } + + pub fn pop_number(&mut self) -> Result { + let val = self.pop()?; + val.as_float().ok_or_else(|| "Expected number".into()) + } + + pub fn pop_integer(&mut self) -> Result { + let val = self.pop()?; + if let Value::Boolean(b) = val { + return Ok(if b { 1 } else { 0 }); + } + val.as_integer().ok_or_else(|| "Expected integer".into()) + } + + pub fn peek(&self) -> Result<&Value, String> { + self.operand_stack.last().ok_or("Stack underflow".into()) + } + + pub(super) fn pop_trap>( + &mut self, + opcode: OpCode, + pc: u32, + message: S, + ) -> Result { + self.pop().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc)) + } + + pub(super) fn peek_trap>( + &self, + opcode: OpCode, + pc: u32, + message: S, + ) -> Result<&Value, LogicalFrameEndingReason> { + self.peek().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc)) + } + + pub(super) fn binary_op( + &mut self, + opcode: OpCode, + start_pc: u32, + f: F, + ) -> Result<(), LogicalFrameEndingReason> + where + F: FnOnce(Value, Value) -> Result, + { + let b = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?; + let a = self.pop_trap(opcode, start_pc, format!("{:?} requires two operands", opcode))?; + match f(a, b) { + Ok(res) => { + self.push(res); + Ok(()) + } + Err(OpError::Trap(code, msg)) => Err(self.trap(code, opcode as u16, msg, start_pc)), + } + } +}