break monster VM into small pieces :D

This commit is contained in:
bQUARKz 2026-03-03 16:42:23 +00:00
parent ea12bd3fd5
commit 9a60754e5a
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
24 changed files with 2894 additions and 3514 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Item = &'static SyscallRegistryEntry> {
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())
}

View File

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

View File

@ -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<Self> {
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",
}
}
}

View File

@ -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<SyscallResolved> {
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<SyscallResolved> {
resolve_syscall_impl(module, name, version)
}
pub fn resolve_program_syscalls(
declared: &[SyscallIdentity],
caps: CapFlags,
) -> Result<Vec<SyscallResolved>, 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<Vec<SyscallResolved>, 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)
}

View File

@ -0,0 +1,169 @@
use super::*;
fn all_syscalls() -> Vec<Syscall> {
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,
}
);
}

File diff suppressed because it is too large Load Diff

View File

@ -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<String> = 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<String, VmFault> {
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<f64, VmFault> {
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))),
}
}

View File

@ -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<CertificationConfig>) -> 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<dyn FsBackend>) {
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)
}
}
}
}

View File

@ -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<u8>, 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<u8>, syscalls: Vec<SyscallDecl>) -> Vec<u8> {
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::<Value>::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 { .. })));
}

View File

@ -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<CrashReport> {
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<CrashReport> {
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();
}
}

View File

@ -1,3 +1,8 @@
mod gc;
mod loader;
mod runtime;
mod stack;
use crate::call_frame::CallFrame; use crate::call_frame::CallFrame;
use crate::heap::{CoroutineState, Heap}; use crate::heap::{CoroutineState, Heap};
use crate::lookup_intrinsic_by_id; use crate::lookup_intrinsic_by_id;
@ -20,73 +25,6 @@ use prometeu_bytecode::{
use prometeu_hal::syscalls::caps::NONE; use prometeu_hal::syscalls::caps::NONE;
use prometeu_hal::vm_fault::VmFault; 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. /// 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 /// This allows the system to decide if it should continue execution in the next tick
/// or if the frame is finalized. /// 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<u8>,
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::<usize>() {
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::<usize>() {
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<BudgetReport, String> {
let start_cycles = self.cycles;
let mut steps_executed = 0;
let mut ending_reason: Option<LogicalFrameEndingReason> = 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<Vec<BudgetReport>, 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<Vec<BudgetReport>, 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<Vec<BudgetReport>, 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). /// Executes a single instruction at the current Program Counter (PC).
/// ///
/// This follows the classic CPU cycle: /// This follows the classic CPU cycle:
@ -1535,249 +1169,6 @@ impl VirtualMachine {
self.cycles += opcode.cycles(); self.cycles += opcode.cycles();
Ok(()) 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<prometeu_bytecode::HeapRef>);
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<Value, String> {
self.operand_stack.pop().ok_or("Stack underflow".into())
}
pub fn pop_number(&mut self) -> Result<f64, String> {
let val = self.pop()?;
val.as_float().ok_or_else(|| "Expected number".into())
}
pub fn pop_integer(&mut self) -> Result<i64, String> {
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<S: Into<String>>(
&mut self,
opcode: OpCode,
pc: u32,
message: S,
) -> Result<Value, LogicalFrameEndingReason> {
self.pop().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc))
}
fn peek_trap<S: Into<String>>(
&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<F>(
&mut self,
opcode: OpCode,
start_pc: u32,
f: F,
) -> Result<(), LogicalFrameEndingReason>
where
F: FnOnce(Value, Value) -> Result<Value, OpError>,
{
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<V: RootVisitor + ?Sized>(&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)] #[cfg(test)]

View File

@ -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<prometeu_bytecode::HeapRef>);
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<V: RootVisitor + ?Sized>(&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);
}
}
}

View File

@ -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<u8>,
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::<usize>() {
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::<usize>() {
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);
}
}

View File

@ -0,0 +1,129 @@
use super::*;
impl VirtualMachine {
pub fn run_budget(
&mut self,
budget: u64,
native: &mut dyn NativeInterface,
ctx: &mut HostContext,
) -> Result<BudgetReport, String> {
let start_cycles = self.cycles;
let mut steps_executed = 0;
let mut ending_reason: Option<LogicalFrameEndingReason> = 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<Vec<BudgetReport>, 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<Vec<BudgetReport>, 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<Vec<BudgetReport>, 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)
}
}

View File

@ -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<Value, String> {
self.operand_stack.pop().ok_or("Stack underflow".into())
}
pub fn pop_number(&mut self) -> Result<f64, String> {
let val = self.pop()?;
val.as_float().ok_or_else(|| "Expected number".into())
}
pub fn pop_integer(&mut self) -> Result<i64, String> {
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<S: Into<String>>(
&mut self,
opcode: OpCode,
pc: u32,
message: S,
) -> Result<Value, LogicalFrameEndingReason> {
self.pop().map_err(|_| self.trap(TRAP_STACK_UNDERFLOW, opcode as u16, message.into(), pc))
}
pub(super) fn peek_trap<S: Into<String>>(
&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<F>(
&mut self,
opcode: OpCode,
start_pc: u32,
f: F,
) -> Result<(), LogicalFrameEndingReason>
where
F: FnOnce(Value, Value) -> Result<Value, OpError>,
{
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)),
}
}
}