break monster VM into small pieces :D
This commit is contained in:
parent
ea12bd3fd5
commit
9a60754e5a
File diff suppressed because it is too large
Load Diff
12
crates/console/prometeu-hal/src/syscalls/caps.rs
Normal file
12
crates/console/prometeu-hal/src/syscalls/caps.rs
Normal 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;
|
||||||
53
crates/console/prometeu-hal/src/syscalls/domains/asset.rs
Normal file
53
crates/console/prometeu-hal/src/syscalls/domains/asset.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
29
crates/console/prometeu-hal/src/syscalls/domains/audio.rs
Normal file
29
crates/console/prometeu-hal/src/syscalls/domains/audio.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
29
crates/console/prometeu-hal/src/syscalls/domains/bank.rs
Normal file
29
crates/console/prometeu-hal/src/syscalls/domains/bank.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
78
crates/console/prometeu-hal/src/syscalls/domains/fs.rs
Normal file
78
crates/console/prometeu-hal/src/syscalls/domains/fs.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
113
crates/console/prometeu-hal/src/syscalls/domains/gfx.rs
Normal file
113
crates/console/prometeu-hal/src/syscalls/domains/gfx.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
305
crates/console/prometeu-hal/src/syscalls/domains/input.rs
Normal file
305
crates/console/prometeu-hal/src/syscalls/domains/input.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
29
crates/console/prometeu-hal/src/syscalls/domains/log.rs
Normal file
29
crates/console/prometeu-hal/src/syscalls/domains/log.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
51
crates/console/prometeu-hal/src/syscalls/domains/mod.rs
Normal file
51
crates/console/prometeu-hal/src/syscalls/domains/mod.rs
Normal 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())
|
||||||
|
}
|
||||||
29
crates/console/prometeu-hal/src/syscalls/domains/system.rs
Normal file
29
crates/console/prometeu-hal/src/syscalls/domains/system.rs
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
137
crates/console/prometeu-hal/src/syscalls/registry.rs
Normal file
137
crates/console/prometeu-hal/src/syscalls/registry.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
crates/console/prometeu-hal/src/syscalls/resolver.rs
Normal file
156
crates/console/prometeu-hal/src/syscalls/resolver.rs
Normal 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)
|
||||||
|
}
|
||||||
169
crates/console/prometeu-hal/src/syscalls/tests.rs
Normal file
169
crates/console/prometeu-hal/src/syscalls/tests.rs
Normal 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
@ -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))),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 { .. })));
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)]
|
||||||
|
|||||||
115
crates/console/prometeu-vm/src/virtual_machine/gc.rs
Normal file
115
crates/console/prometeu-vm/src/virtual_machine/gc.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
crates/console/prometeu-vm/src/virtual_machine/loader.rs
Normal file
182
crates/console/prometeu-vm/src/virtual_machine/loader.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
crates/console/prometeu-vm/src/virtual_machine/runtime.rs
Normal file
129
crates/console/prometeu-vm/src/virtual_machine/runtime.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crates/console/prometeu-vm/src/virtual_machine/stack.rs
Normal file
76
crates/console/prometeu-vm/src/virtual_machine/stack.rs
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user