adjustments on capabilities on cartridges

This commit is contained in:
bQUARKz 2026-03-02 14:02:05 +00:00
parent cfdca93160
commit d5ef8a2003
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
19 changed files with 1570 additions and 205 deletions

View File

@ -1,22 +1,22 @@
mod abi;
pub mod assembler;
mod decoder;
mod disassembler;
pub mod isa; // canonical ISA boundary (core and future profiles)
mod layout;
pub mod model;
mod opcode;
mod opcode_spec;
mod program_image;
mod value;
pub mod isa; // canonical ISA boundary (core and future profiles)
mod disassembler;
pub mod assembler;
pub use abi::{
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC,
TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE,
};
pub use assembler::{assemble, AsmError};
pub use decoder::{decode_next, DecodeError};
pub use disassembler::disassemble;
pub use assembler::{assemble, AsmError};
pub use layout::{compute_function_layouts, FunctionLayout};
pub use model::{BytecodeLoader, FunctionMeta, LoadError};
pub use program_image::ProgramImage;

View File

@ -1,9 +1,11 @@
use prometeu_bytecode::{assemble, disassemble};
use prometeu_bytecode::isa::core::CoreOpCode;
use prometeu_bytecode::{assemble, disassemble};
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
out.extend_from_slice(&(op as u16).to_le_bytes());
if let Some(bytes) = imm { out.extend_from_slice(bytes); }
if let Some(bytes) = imm {
out.extend_from_slice(bytes);
}
}
#[test]

View File

@ -3,7 +3,9 @@ use prometeu_bytecode::isa::core::CoreOpCode;
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
out.extend_from_slice(&(op as u16).to_le_bytes());
if let Some(bytes) = imm { out.extend_from_slice(bytes); }
if let Some(bytes) = imm {
out.extend_from_slice(bytes);
}
}
#[test]

View File

@ -1,4 +1,5 @@
use crate::asset::{AssetEntry, PreloadEntry};
use crate::syscalls::CapFlags;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
@ -14,6 +15,7 @@ pub struct Cartridge {
pub app_version: String,
pub app_mode: AppMode,
pub entrypoint: String,
pub capabilities: CapFlags,
pub program: Vec<u8>,
pub assets: Vec<u8>,
pub asset_table: Vec<AssetEntry>,
@ -27,6 +29,7 @@ pub struct CartridgeDTO {
pub app_version: String,
pub app_mode: AppMode,
pub entrypoint: String,
pub capabilities: CapFlags,
pub program: Vec<u8>,
pub assets: Vec<u8>,
#[serde(default)]
@ -43,6 +46,7 @@ impl From<CartridgeDTO> for Cartridge {
app_version: dto.app_version,
app_mode: dto.app_mode,
entrypoint: dto.entrypoint,
capabilities: dto.capabilities,
program: dto.program,
assets: dto.assets,
asset_table: dto.asset_table,
@ -61,6 +65,21 @@ pub enum CartridgeError {
IoError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
None,
System,
Gfx,
Input,
Audio,
Fs,
Log,
Asset,
Bank,
All,
}
#[derive(Deserialize)]
pub struct CartridgeManifest {
pub magic: String,
@ -71,6 +90,8 @@ pub struct CartridgeManifest {
pub app_mode: AppMode,
pub entrypoint: String,
#[serde(default)]
pub capabilities: Vec<Capability>,
#[serde(default)]
pub asset_table: Vec<AssetEntry>,
#[serde(default)]
pub preload: Vec<PreloadEntry>,

View File

@ -1,4 +1,6 @@
use crate::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use crate::cartridge::{Capability, Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use crate::syscalls::{CapFlags, caps};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
@ -43,6 +45,8 @@ impl DirectoryCartridgeLoader {
return Err(CartridgeError::UnsupportedVersion);
}
let capabilities = normalize_capabilities(&manifest.capabilities)?;
let program_path = path.join("program.pbx");
if !program_path.exists() {
return Err(CartridgeError::MissingProgram);
@ -63,6 +67,7 @@ impl DirectoryCartridgeLoader {
app_version: manifest.app_version,
app_mode: manifest.app_mode,
entrypoint: manifest.entrypoint,
capabilities,
program,
assets,
asset_table: manifest.asset_table,
@ -80,3 +85,157 @@ impl PackedCartridgeLoader {
Err(CartridgeError::InvalidFormat)
}
}
fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, CartridgeError> {
let mut seen = HashSet::new();
let mut normalized = caps::NONE;
for capability in capabilities {
if !seen.insert(*capability) {
return Err(CartridgeError::InvalidManifest);
}
normalized |= match capability {
Capability::None => caps::NONE,
Capability::System => caps::SYSTEM,
Capability::Gfx => caps::GFX,
Capability::Input => caps::INPUT,
Capability::Audio => caps::AUDIO,
Capability::Fs => caps::FS,
Capability::Log => caps::LOG,
Capability::Asset => caps::ASSET,
Capability::Bank => caps::BANK,
Capability::All => caps::ALL,
};
}
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestCartridgeDir {
path: PathBuf,
}
impl TestCartridgeDir {
fn new(manifest: serde_json::Value) -> Self {
let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time must be after unix epoch")
.as_nanos();
let path = std::env::temp_dir()
.join(format!("prometeu-hal-cartridge-loader-{}-{}", timestamp, unique));
fs::create_dir_all(&path).expect("must create temporary cartridge directory");
fs::write(
path.join("manifest.json"),
serde_json::to_vec_pretty(&manifest).expect("manifest must serialize"),
)
.expect("must write manifest.json");
fs::write(path.join("program.pbx"), [0x01_u8, 0x02, 0x03]).expect("must write program");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestCartridgeDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn manifest_with_capabilities(capabilities: Option<Vec<&str>>) -> serde_json::Value {
let mut manifest = json!({
"magic": "PMTU",
"cartridge_version": 1,
"app_id": 1001,
"title": "Example",
"app_version": "1.0.0",
"app_mode": "Game",
"entrypoint": "main"
});
if let Some(capabilities) = capabilities {
manifest["capabilities"] = json!(capabilities);
}
manifest
}
#[test]
fn load_without_capabilities_defaults_to_none() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(None));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::NONE);
}
#[test]
fn load_with_single_capability_normalizes_to_flag() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::GFX);
}
#[test]
fn load_with_multiple_capabilities_combines_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "input"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::GFX | caps::INPUT);
}
#[test]
fn load_with_all_capability_normalizes_to_all_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["all"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::ALL);
}
#[test]
fn load_with_none_capability_keeps_zero_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["none"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::NONE);
}
#[test]
fn load_with_duplicate_capabilities_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "gfx"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
#[test]
fn load_with_unknown_capability_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["network"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
}

View File

@ -226,11 +226,7 @@ pub struct SyscallResolved {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadError {
/// The (module, name, version) triple is not known by the host.
UnknownSyscall {
module: &'static str,
name: &'static str,
version: u16,
},
UnknownSyscall { module: &'static str, name: &'static str, version: u16 },
/// The cartridge lacks required capabilities for the syscall.
MissingCapability {
required: CapFlags,
@ -244,7 +240,11 @@ pub enum LoadError {
/// Resolve a canonical syscall identity to its numeric id and metadata.
///
/// Returns `None` if the identity is unknown.
pub fn resolve_syscall(module: &'static str, name: &'static str, version: u16) -> Option<SyscallResolved> {
pub fn resolve_syscall(
module: &'static str,
name: &'static str,
version: u16,
) -> Option<SyscallResolved> {
for entry in SYSCALL_TABLE {
if entry.meta.module == module && entry.meta.name == name && entry.meta.version == version {
return Some(SyscallResolved { id: entry.meta.id, meta: entry.meta });
@ -266,7 +266,11 @@ pub fn resolve_program_syscalls(
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 });
return Err(LoadError::UnknownSyscall {
module: ident.module,
name: ident.name,
version: ident.version,
});
};
// Capability gating: required must be subset of provided
let missing = res.meta.caps & !caps;
@ -291,74 +295,808 @@ pub fn resolve_program_syscalls(
/// this table.
pub const SYSCALL_TABLE: &[SyscallRegistryEntry] = &[
// --- System ---
SyscallRegistryEntry { syscall: Syscall::SystemHasCart, meta: SyscallMeta { id: 0x0001, module: "system", name: "has_cart", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::SYSTEM, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::SystemRunCart, meta: SyscallMeta { id: 0x0002, module: "system", name: "run_cart", version: 1, arg_slots: 0, ret_slots: 0, caps: caps::SYSTEM, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 50 } },
SyscallRegistryEntry {
syscall: Syscall::SystemHasCart,
meta: SyscallMeta {
id: 0x0001,
module: "system",
name: "has_cart",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::SYSTEM,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::SystemRunCart,
meta: SyscallMeta {
id: 0x0002,
module: "system",
name: "run_cart",
version: 1,
arg_slots: 0,
ret_slots: 0,
caps: caps::SYSTEM,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 50,
},
},
// --- GFX ---
SyscallRegistryEntry { syscall: Syscall::GfxClear, meta: SyscallMeta { id: 0x1001, module: "gfx", name: "clear", version: 1, arg_slots: 1, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::GfxFillRect, meta: SyscallMeta { id: 0x1002, module: "gfx", name: "fill_rect", version: 1, arg_slots: 5, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawLine, meta: SyscallMeta { id: 0x1003, module: "gfx", name: "draw_line", version: 1, arg_slots: 5, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawCircle, meta: SyscallMeta { id: 0x1004, module: "gfx", name: "draw_circle", version: 1, arg_slots: 4, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawDisc, meta: SyscallMeta { id: 0x1005, module: "gfx", name: "draw_disc", version: 1, arg_slots: 5, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawSquare, meta: SyscallMeta { id: 0x1006, module: "gfx", name: "draw_square", version: 1, arg_slots: 6, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::GfxSetSprite, meta: SyscallMeta { id: 0x1007, module: "gfx", name: "set_sprite", version: 1, arg_slots: 10, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::GfxDrawText, meta: SyscallMeta { id: 0x1008, module: "gfx", name: "draw_text", version: 1, arg_slots: 4, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::GfxClear565, meta: SyscallMeta { id: 0x1010, module: "gfx", name: "clear_565", version: 1, arg_slots: 1, ret_slots: 0, caps: caps::GFX, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry {
syscall: Syscall::GfxClear,
meta: SyscallMeta {
id: 0x1001,
module: "gfx",
name: "clear",
version: 1,
arg_slots: 1,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxFillRect,
meta: SyscallMeta {
id: 0x1002,
module: "gfx",
name: "fill_rect",
version: 1,
arg_slots: 5,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxDrawLine,
meta: SyscallMeta {
id: 0x1003,
module: "gfx",
name: "draw_line",
version: 1,
arg_slots: 5,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxDrawCircle,
meta: SyscallMeta {
id: 0x1004,
module: "gfx",
name: "draw_circle",
version: 1,
arg_slots: 4,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxDrawDisc,
meta: SyscallMeta {
id: 0x1005,
module: "gfx",
name: "draw_disc",
version: 1,
arg_slots: 5,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxDrawSquare,
meta: SyscallMeta {
id: 0x1006,
module: "gfx",
name: "draw_square",
version: 1,
arg_slots: 6,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxSetSprite,
meta: SyscallMeta {
id: 0x1007,
module: "gfx",
name: "set_sprite",
version: 1,
arg_slots: 10,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxDrawText,
meta: SyscallMeta {
id: 0x1008,
module: "gfx",
name: "draw_text",
version: 1,
arg_slots: 4,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::GfxClear565,
meta: SyscallMeta {
id: 0x1010,
module: "gfx",
name: "clear_565",
version: 1,
arg_slots: 1,
ret_slots: 0,
caps: caps::GFX,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 20,
},
},
// --- Input ---
SyscallRegistryEntry { syscall: Syscall::InputGetPad, meta: SyscallMeta { id: 0x2001, module: "input", name: "get_pad", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::InputGetPadPressed, meta: SyscallMeta { id: 0x2002, module: "input", name: "get_pad_pressed", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::InputGetPadReleased, meta: SyscallMeta { id: 0x2003, module: "input", name: "get_pad_released", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::InputGetPadHold, meta: SyscallMeta { id: 0x2004, module: "input", name: "get_pad_hold", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::InputPadSnapshot, meta: SyscallMeta { id: 0x2010, module: "input", name: "pad_snapshot", version: 1, arg_slots: 0, ret_slots: 48, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::InputTouchSnapshot, meta: SyscallMeta { id: 0x2011, module: "input", name: "touch_snapshot", version: 1, arg_slots: 0, ret_slots: 6, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchGetX, meta: SyscallMeta { id: 0x2101, module: "input", name: "touch_get_x", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchGetY, meta: SyscallMeta { id: 0x2102, module: "input", name: "touch_get_y", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchIsDown, meta: SyscallMeta { id: 0x2103, module: "input", name: "touch_is_down", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchIsPressed, meta: SyscallMeta { id: 0x2104, module: "input", name: "touch_is_pressed", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchIsReleased, meta: SyscallMeta { id: 0x2105, module: "input", name: "touch_is_released", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchGetHold, meta: SyscallMeta { id: 0x2106, module: "input", name: "touch_get_hold", version: 1, arg_slots: 0, ret_slots: 1, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::TouchGetFinger, meta: SyscallMeta { id: 0x2107, module: "input", name: "touch_get_finger", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry {
syscall: Syscall::InputGetPad,
meta: SyscallMeta {
id: 0x2001,
module: "input",
name: "get_pad",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::InputGetPadPressed,
meta: SyscallMeta {
id: 0x2002,
module: "input",
name: "get_pad_pressed",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::InputGetPadReleased,
meta: SyscallMeta {
id: 0x2003,
module: "input",
name: "get_pad_released",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::InputGetPadHold,
meta: SyscallMeta {
id: 0x2004,
module: "input",
name: "get_pad_hold",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::InputPadSnapshot,
meta: SyscallMeta {
id: 0x2010,
module: "input",
name: "pad_snapshot",
version: 1,
arg_slots: 0,
ret_slots: 48,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::InputTouchSnapshot,
meta: SyscallMeta {
id: 0x2011,
module: "input",
name: "touch_snapshot",
version: 1,
arg_slots: 0,
ret_slots: 6,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchGetX,
meta: SyscallMeta {
id: 0x2101,
module: "input",
name: "touch_get_x",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchGetY,
meta: SyscallMeta {
id: 0x2102,
module: "input",
name: "touch_get_y",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchIsDown,
meta: SyscallMeta {
id: 0x2103,
module: "input",
name: "touch_is_down",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchIsPressed,
meta: SyscallMeta {
id: 0x2104,
module: "input",
name: "touch_is_pressed",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchIsReleased,
meta: SyscallMeta {
id: 0x2105,
module: "input",
name: "touch_is_released",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchGetHold,
meta: SyscallMeta {
id: 0x2106,
module: "input",
name: "touch_get_hold",
version: 1,
arg_slots: 0,
ret_slots: 1,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::TouchGetFinger,
meta: SyscallMeta {
id: 0x2107,
module: "input",
name: "touch_get_finger",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
// --- Input (Pad service-based) ---
SyscallRegistryEntry { syscall: Syscall::PadGetUp, meta: SyscallMeta { id: 0x2200, module: "input", name: "pad_get_up", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetDown, meta: SyscallMeta { id: 0x2201, module: "input", name: "pad_get_down", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetLeft, meta: SyscallMeta { id: 0x2202, module: "input", name: "pad_get_left", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetRight, meta: SyscallMeta { id: 0x2203, module: "input", name: "pad_get_right", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetA, meta: SyscallMeta { id: 0x2204, module: "input", name: "pad_get_a", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetB, meta: SyscallMeta { id: 0x2205, module: "input", name: "pad_get_b", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetX, meta: SyscallMeta { id: 0x2206, module: "input", name: "pad_get_x", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetY, meta: SyscallMeta { id: 0x2207, module: "input", name: "pad_get_y", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetL, meta: SyscallMeta { id: 0x2208, module: "input", name: "pad_get_l", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetR, meta: SyscallMeta { id: 0x2209, module: "input", name: "pad_get_r", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetStart, meta: SyscallMeta { id: 0x220A, module: "input", name: "pad_get_start", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::PadGetSelect, meta: SyscallMeta { id: 0x220B, module: "input", name: "pad_get_select", version: 1, arg_slots: 0, ret_slots: 4, caps: caps::INPUT, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry {
syscall: Syscall::PadGetUp,
meta: SyscallMeta {
id: 0x2200,
module: "input",
name: "pad_get_up",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetDown,
meta: SyscallMeta {
id: 0x2201,
module: "input",
name: "pad_get_down",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetLeft,
meta: SyscallMeta {
id: 0x2202,
module: "input",
name: "pad_get_left",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetRight,
meta: SyscallMeta {
id: 0x2203,
module: "input",
name: "pad_get_right",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetA,
meta: SyscallMeta {
id: 0x2204,
module: "input",
name: "pad_get_a",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetB,
meta: SyscallMeta {
id: 0x2205,
module: "input",
name: "pad_get_b",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetX,
meta: SyscallMeta {
id: 0x2206,
module: "input",
name: "pad_get_x",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetY,
meta: SyscallMeta {
id: 0x2207,
module: "input",
name: "pad_get_y",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetL,
meta: SyscallMeta {
id: 0x2208,
module: "input",
name: "pad_get_l",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetR,
meta: SyscallMeta {
id: 0x2209,
module: "input",
name: "pad_get_r",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetStart,
meta: SyscallMeta {
id: 0x220A,
module: "input",
name: "pad_get_start",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::PadGetSelect,
meta: SyscallMeta {
id: 0x220B,
module: "input",
name: "pad_get_select",
version: 1,
arg_slots: 0,
ret_slots: 4,
caps: caps::INPUT,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
// --- Audio ---
SyscallRegistryEntry { syscall: Syscall::AudioPlaySample, meta: SyscallMeta { id: 0x3001, module: "audio", name: "play_sample", version: 1, arg_slots: 5, ret_slots: 0, caps: caps::AUDIO, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::AudioPlay, meta: SyscallMeta { id: 0x3002, module: "audio", name: "play", version: 1, arg_slots: 7, ret_slots: 0, caps: caps::AUDIO, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry {
syscall: Syscall::AudioPlaySample,
meta: SyscallMeta {
id: 0x3001,
module: "audio",
name: "play_sample",
version: 1,
arg_slots: 5,
ret_slots: 0,
caps: caps::AUDIO,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::AudioPlay,
meta: SyscallMeta {
id: 0x3002,
module: "audio",
name: "play",
version: 1,
arg_slots: 7,
ret_slots: 0,
caps: caps::AUDIO,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
// --- FS ---
SyscallRegistryEntry { syscall: Syscall::FsOpen, meta: SyscallMeta { id: 0x4001, module: "fs", name: "open", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::FsRead, meta: SyscallMeta { id: 0x4002, module: "fs", name: "read", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::FsWrite, meta: SyscallMeta { id: 0x4003, module: "fs", name: "write", version: 1, arg_slots: 2, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::FsClose, meta: SyscallMeta { id: 0x4004, module: "fs", name: "close", version: 1, arg_slots: 1, ret_slots: 0, caps: caps::FS, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::FsListDir, meta: SyscallMeta { id: 0x4005, module: "fs", name: "list_dir", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::FsExists, meta: SyscallMeta { id: 0x4006, module: "fs", name: "exists", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::FS, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::FsDelete, meta: SyscallMeta { id: 0x4007, module: "fs", name: "delete", version: 1, arg_slots: 1, ret_slots: 0, caps: caps::FS, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry {
syscall: Syscall::FsOpen,
meta: SyscallMeta {
id: 0x4001,
module: "fs",
name: "open",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::FS,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::FsRead,
meta: SyscallMeta {
id: 0x4002,
module: "fs",
name: "read",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::FS,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::FsWrite,
meta: SyscallMeta {
id: 0x4003,
module: "fs",
name: "write",
version: 1,
arg_slots: 2,
ret_slots: 1,
caps: caps::FS,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::FsClose,
meta: SyscallMeta {
id: 0x4004,
module: "fs",
name: "close",
version: 1,
arg_slots: 1,
ret_slots: 0,
caps: caps::FS,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::FsListDir,
meta: SyscallMeta {
id: 0x4005,
module: "fs",
name: "list_dir",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::FS,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::FsExists,
meta: SyscallMeta {
id: 0x4006,
module: "fs",
name: "exists",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::FS,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::FsDelete,
meta: SyscallMeta {
id: 0x4007,
module: "fs",
name: "delete",
version: 1,
arg_slots: 1,
ret_slots: 0,
caps: caps::FS,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
// --- Log ---
SyscallRegistryEntry { syscall: Syscall::LogWrite, meta: SyscallMeta { id: 0x5001, module: "log", name: "write", version: 1, arg_slots: 2, ret_slots: 0, caps: caps::LOG, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry { syscall: Syscall::LogWriteTag, meta: SyscallMeta { id: 0x5002, module: "log", name: "write_tag", version: 1, arg_slots: 3, ret_slots: 0, caps: caps::LOG, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 5 } },
SyscallRegistryEntry {
syscall: Syscall::LogWrite,
meta: SyscallMeta {
id: 0x5001,
module: "log",
name: "write",
version: 1,
arg_slots: 2,
ret_slots: 0,
caps: caps::LOG,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 5,
},
},
SyscallRegistryEntry {
syscall: Syscall::LogWriteTag,
meta: SyscallMeta {
id: 0x5002,
module: "log",
name: "write_tag",
version: 1,
arg_slots: 3,
ret_slots: 0,
caps: caps::LOG,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 5,
},
},
// --- Asset/Bank ---
SyscallRegistryEntry { syscall: Syscall::AssetLoad, meta: SyscallMeta { id: 0x6001, module: "asset", name: "load", version: 1, arg_slots: 3, ret_slots: 1, caps: caps::ASSET, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::AssetStatus, meta: SyscallMeta { id: 0x6002, module: "asset", name: "status", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::ASSET, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::AssetCommit, meta: SyscallMeta { id: 0x6003, module: "asset", name: "commit", version: 1, arg_slots: 1, ret_slots: 0, caps: caps::ASSET, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::AssetCancel, meta: SyscallMeta { id: 0x6004, module: "asset", name: "cancel", version: 1, arg_slots: 1, ret_slots: 0, caps: caps::ASSET, determinism: Determinism::NonDeterministic, may_allocate: false, cost_hint: 20 } },
SyscallRegistryEntry { syscall: Syscall::BankInfo, meta: SyscallMeta { id: 0x6101, module: "bank", name: "info", version: 1, arg_slots: 1, ret_slots: 1, caps: caps::BANK, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry { syscall: Syscall::BankSlotInfo, meta: SyscallMeta { id: 0x6102, module: "bank", name: "slot_info", version: 1, arg_slots: 2, ret_slots: 1, caps: caps::BANK, determinism: Determinism::Deterministic, may_allocate: false, cost_hint: 1 } },
SyscallRegistryEntry {
syscall: Syscall::AssetLoad,
meta: SyscallMeta {
id: 0x6001,
module: "asset",
name: "load",
version: 1,
arg_slots: 3,
ret_slots: 1,
caps: caps::ASSET,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::AssetStatus,
meta: SyscallMeta {
id: 0x6002,
module: "asset",
name: "status",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::ASSET,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::AssetCommit,
meta: SyscallMeta {
id: 0x6003,
module: "asset",
name: "commit",
version: 1,
arg_slots: 1,
ret_slots: 0,
caps: caps::ASSET,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::AssetCancel,
meta: SyscallMeta {
id: 0x6004,
module: "asset",
name: "cancel",
version: 1,
arg_slots: 1,
ret_slots: 0,
caps: caps::ASSET,
determinism: Determinism::NonDeterministic,
may_allocate: false,
cost_hint: 20,
},
},
SyscallRegistryEntry {
syscall: Syscall::BankInfo,
meta: SyscallMeta {
id: 0x6101,
module: "bank",
name: "info",
version: 1,
arg_slots: 1,
ret_slots: 1,
caps: caps::BANK,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
SyscallRegistryEntry {
syscall: Syscall::BankSlotInfo,
meta: SyscallMeta {
id: 0x6102,
module: "bank",
name: "slot_info",
version: 1,
arg_slots: 2,
ret_slots: 1,
caps: caps::BANK,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
},
];
/// Returns the metadata associated with this syscall.
@ -593,8 +1331,13 @@ mod tests {
// (module,name,version) must be unique
let key = (e.meta.module, e.meta.name, e.meta.version);
assert!(identities.insert(key), "duplicate canonical identity: ({}.{}, v{})",
e.meta.module, e.meta.name, e.meta.version);
assert!(
identities.insert(key),
"duplicate canonical identity: ({}.{}, v{})",
e.meta.module,
e.meta.name,
e.meta.version
);
}
// 3) Table and explicit list sizes must match (guard against omissions).

View File

@ -1,5 +1,5 @@
use crate::object::{ObjectHeader, ObjectKind};
use crate::call_frame::CallFrame;
use crate::object::{ObjectHeader, ObjectKind};
use prometeu_bytecode::{HeapRef, Value};
/// Internal stored object: header plus opaque payload bytes.
@ -50,14 +50,22 @@ pub struct Heap {
}
impl Heap {
pub fn new() -> Self { Self { objects: Vec::new() } }
pub fn new() -> Self {
Self { objects: Vec::new() }
}
/// Allocate a new object with the given kind and raw payload bytes.
/// Returns an opaque `HeapRef` handle.
#[cfg(test)]
pub fn allocate_object(&mut self, kind: ObjectKind, payload: &[u8]) -> HeapRef {
let header = ObjectHeader::new(kind, payload.len() as u32);
let obj = StoredObject { header, payload: payload.to_vec(), array_elems: None, closure_env: None, coroutine: None };
let obj = StoredObject {
header,
payload: payload.to_vec(),
array_elems: None,
closure_env: None,
coroutine: None,
};
let idx = self.objects.len();
// No free-list reuse in this PR: append and keep indices stable.
self.objects.push(Some(obj));
@ -69,7 +77,13 @@ impl Heap {
#[cfg(test)]
pub fn allocate_array(&mut self, elements: Vec<Value>) -> HeapRef {
let header = ObjectHeader::new(ObjectKind::Array, elements.len() as u32);
let obj = StoredObject { header, payload: Vec::new(), array_elems: Some(elements), closure_env: None, coroutine: None };
let obj = StoredObject {
header,
payload: Vec::new(),
array_elems: Some(elements),
closure_env: None,
coroutine: None,
};
let idx = self.objects.len();
// No free-list reuse in this PR: append and keep indices stable.
self.objects.push(Some(obj));
@ -125,7 +139,9 @@ impl Heap {
/// Returns true if this handle refers to an allocated object.
pub fn is_valid(&self, r: HeapRef) -> bool {
let idx = r.0 as usize;
if idx >= self.objects.len() { return false; }
if idx >= self.objects.len() {
return false;
}
self.objects[idx].is_some()
}
@ -147,18 +163,12 @@ impl Heap {
/// Get immutable access to an object's header by handle.
pub fn header(&self, r: HeapRef) -> Option<&ObjectHeader> {
self.objects
.get(r.0 as usize)
.and_then(|slot| slot.as_ref())
.map(|o| &o.header)
self.objects.get(r.0 as usize).and_then(|slot| slot.as_ref()).map(|o| &o.header)
}
/// Internal: get mutable access to an object's header by handle.
fn header_mut(&mut self, r: HeapRef) -> Option<&mut ObjectHeader> {
self.objects
.get_mut(r.0 as usize)
.and_then(|slot| slot.as_mut())
.map(|o| &mut o.header)
self.objects.get_mut(r.0 as usize).and_then(|slot| slot.as_mut()).map(|o| &mut o.header)
}
// Internal: list inner `HeapRef` children of an object without allocating.
@ -215,8 +225,12 @@ impl Heap {
pub fn closure_fn_id(&self, r: HeapRef) -> Option<u32> {
let idx = r.0 as usize;
let slot = self.objects.get(idx)?.as_ref()?;
if slot.header.kind != ObjectKind::Closure { return None; }
if slot.payload.len() < 8 { return None; }
if slot.header.kind != ObjectKind::Closure {
return None;
}
if slot.payload.len() < 8 {
return None;
}
debug_assert_eq!(slot.header.payload_len, 8);
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&slot.payload[0..4]);
@ -228,7 +242,9 @@ impl Heap {
pub fn closure_env_slice(&self, r: HeapRef) -> Option<&[Value]> {
let idx = r.0 as usize;
let slot = self.objects.get(idx)?.as_ref()?;
if slot.header.kind != ObjectKind::Closure { return None; }
if slot.header.kind != ObjectKind::Closure {
return None;
}
if slot.payload.len() >= 8 {
let mut nbytes = [0u8; 4];
nbytes.copy_from_slice(&slot.payload[4..8]);
@ -246,17 +262,21 @@ impl Heap {
let mut stack: Vec<HeapRef> = roots.into_iter().collect();
while let Some(r) = stack.pop() {
if !self.is_valid(r) { continue; }
if !self.is_valid(r) {
continue;
}
// If already marked, skip.
let already_marked = self
.header(r)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if already_marked { continue; }
let already_marked =
self.header(r).map(|h: &ObjectHeader| h.is_marked()).unwrap_or(false);
if already_marked {
continue;
}
// Set mark bit.
if let Some(h) = self.header_mut(r) { h.set_marked(true); }
if let Some(h) = self.header_mut(r) {
h.set_marked(true);
}
// Push children by scanning payload directly (no intermediate Vec allocs).
let idx = r.0 as usize;
@ -272,7 +292,9 @@ impl Heap {
.header(*child)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if !marked { stack.push(*child); }
if !marked {
stack.push(*child);
}
}
}
}
@ -283,7 +305,11 @@ impl Heap {
nbytes.copy_from_slice(&obj.payload[4..8]);
let env_len = u32::from_le_bytes(nbytes) as usize;
if let Some(env) = obj.closure_env.as_ref() {
debug_assert_eq!(env.len(), env_len, "closure env len must match encoded env_len");
debug_assert_eq!(
env.len(),
env_len,
"closure env len must match encoded env_len"
);
for val in env[..env_len].iter() {
if let Value::HeapRef(child) = val
&& self.is_valid(*child)
@ -292,7 +318,9 @@ impl Heap {
.header(*child)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if !marked { stack.push(*child); }
if !marked {
stack.push(*child);
}
}
}
}
@ -307,7 +335,9 @@ impl Heap {
.header(*child)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if !marked { stack.push(*child); }
if !marked {
stack.push(*child);
}
}
}
}
@ -336,7 +366,9 @@ impl Heap {
}
/// Current number of allocated (live) objects.
pub fn len(&self) -> usize { self.objects.iter().filter(|s| s.is_some()).count() }
pub fn len(&self) -> usize {
self.objects.iter().filter(|s| s.is_some()).count()
}
/// Enumerate handles of coroutines that are currently suspended (i.e., not running):
/// Ready or Sleeping. These must be treated as GC roots by the runtime so their
@ -430,11 +462,8 @@ mod tests {
// Target object B (unreferenced yet)
let b = heap.allocate_object(ObjectKind::Bytes, &[9, 9, 9]);
// Array A that contains a reference to B among other primitives
let a = heap.allocate_array(vec![
Value::Int32(1),
Value::HeapRef(b),
Value::Boolean(false),
]);
let a =
heap.allocate_array(vec![Value::Int32(1), Value::HeapRef(b), Value::Boolean(false)]);
// Mark starting from root A
heap.mark_from_roots([a]);

View File

@ -2,16 +2,16 @@ mod call_frame;
mod local_addressing;
// Keep the verifier internal in production builds, but expose it for integration tests
// so the golden verifier suite can exercise it without widening the public API in releases.
mod heap;
mod object;
mod roots;
mod scheduler;
#[cfg(not(test))]
mod verifier;
#[cfg(test)]
mod verifier;
mod virtual_machine;
mod vm_init_error;
mod object;
mod heap;
mod roots;
mod scheduler;
pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};

View File

@ -51,7 +51,8 @@
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ObjectKind {
/// Reserved/unknown kind. Should not appear in valid allocations.
#[allow(dead_code)] // Kept for stable tag layout and persisted images, even if not constructed in this crate yet
#[allow(dead_code)]
// Kept for stable tag layout and persisted images, even if not constructed in this crate yet
Unknown = 0,
/// UTF-8 string. `payload_len` is the number of bytes.
@ -87,7 +88,6 @@ pub enum ObjectKind {
/// contained `HeapRef`s directly.
/// - `payload_len` is 0 for this fixed-layout object.
Coroutine = 6,
// Future kinds must be appended here to keep tag numbers stable.
}
@ -120,10 +120,16 @@ impl ObjectHeader {
}
/// Returns true if the GC mark bit is set.
pub fn is_marked(&self) -> bool { (self.flags & object_flags::MARKED) != 0 }
pub fn is_marked(&self) -> bool {
(self.flags & object_flags::MARKED) != 0
}
/// Sets or clears the GC mark bit. Note: actual GC logic lives elsewhere.
pub fn set_marked(&mut self, value: bool) {
if value { self.flags |= object_flags::MARKED; } else { self.flags &= !object_flags::MARKED; }
if value {
self.flags |= object_flags::MARKED;
} else {
self.flags &= !object_flags::MARKED;
}
}
}

View File

@ -18,9 +18,13 @@ mod tests {
use super::*;
use crate::VirtualMachine;
struct CollectVisitor { pub seen: Vec<HeapRef> }
struct CollectVisitor {
pub seen: Vec<HeapRef>,
}
impl RootVisitor for CollectVisitor {
fn visit_heap_ref(&mut self, r: HeapRef) { self.seen.push(r); }
fn visit_heap_ref(&mut self, r: HeapRef) {
self.seen.push(r);
}
}
#[test]

View File

@ -28,7 +28,9 @@ struct SleepEntry {
}
impl Scheduler {
pub fn new() -> Self { Self::default() }
pub fn new() -> Self {
Self::default()
}
// ---------- Ready queue operations ----------
@ -43,15 +45,25 @@ impl Scheduler {
}
#[cfg(test)]
pub fn is_ready_empty(&self) -> bool { self.ready_queue.is_empty() }
pub fn is_ready_empty(&self) -> bool {
self.ready_queue.is_empty()
}
#[cfg(test)]
pub fn ready_len(&self) -> usize { self.ready_queue.len() }
pub fn ready_len(&self) -> usize {
self.ready_queue.len()
}
// ---------- Current tracking (no switching here) ----------
pub fn set_current(&mut self, coro: Option<HeapRef>) { self.current = coro; }
pub fn set_current(&mut self, coro: Option<HeapRef>) {
self.current = coro;
}
#[cfg(test)]
pub fn current(&self) -> Option<HeapRef> { self.current }
pub fn clear_current(&mut self) { self.current = None; }
pub fn current(&self) -> Option<HeapRef> {
self.current
}
pub fn clear_current(&mut self) {
self.current = None;
}
// ---------- Sleeping operations ----------
@ -63,15 +75,13 @@ impl Scheduler {
self.next_seq = self.next_seq.wrapping_add(1);
// Binary search insertion point by wake_tick, then by seq to keep total order deterministic
let idx = match self
.sleeping
.binary_search_by(|e| {
if e.wake_tick == entry.wake_tick {
e.seq.cmp(&entry.seq)
} else {
e.wake_tick.cmp(&entry.wake_tick)
}
}) {
let idx = match self.sleeping.binary_search_by(|e| {
if e.wake_tick == entry.wake_tick {
e.seq.cmp(&entry.seq)
} else {
e.wake_tick.cmp(&entry.wake_tick)
}
}) {
Ok(i) => i, // equal element position; insert after to preserve FIFO among equals
Err(i) => i,
};
@ -81,10 +91,10 @@ impl Scheduler {
/// Move all sleeping coroutines with `wake_tick <= current_tick` to ready queue (FIFO by wake order).
pub fn wake_ready(&mut self, current_tick: u64) {
// Find split point where wake_tick > current_tick
let split = self
.sleeping
.partition_point(|e| e.wake_tick <= current_tick);
if split == 0 { return; }
let split = self.sleeping.partition_point(|e| e.wake_tick <= current_tick);
if split == 0 {
return;
}
let mut ready_slice: Vec<SleepEntry> = self.sleeping.drain(0..split).collect();
// Already in order by (wake_tick, seq); push in that order to preserve determinism
for e in ready_slice.drain(..) {
@ -93,14 +103,18 @@ impl Scheduler {
}
/// Returns true if there are any sleeping coroutines.
pub fn has_sleeping(&self) -> bool { !self.sleeping.is_empty() }
pub fn has_sleeping(&self) -> bool {
!self.sleeping.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hr(id: u32) -> HeapRef { HeapRef(id) }
fn hr(id: u32) -> HeapRef {
HeapRef(id)
}
#[test]
fn fifo_ready_queue_is_deterministic() {

View File

@ -8,10 +8,9 @@ fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => panic!(
"immediate size mismatch for {:?}: expected {}, got {}",
op, n, bytes.len()
),
(n, Some(bytes)) => {
panic!("immediate size mismatch for {:?}: expected {}, got {}", op, n, bytes.len())
}
(n, None) => panic!("missing immediate for {:?}: need {} bytes", op, n),
}
}

View File

@ -1,6 +1,6 @@
use prometeu_hal::vm_fault::VmFault;
use prometeu_vm::VirtualMachine;
use prometeu_vm::{HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_hal::vm_fault::VmFault;
#[test]
fn gc_collects_unreachable_closure_but_keeps_marked() {

View File

@ -1,6 +1,6 @@
use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
use prometeu_vm::{VirtualMachine, BudgetReport, LogicalFrameEndingReason};
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_vm::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
#[test]
fn scheduler_wake_and_ready_order_is_deterministic() {
@ -13,7 +13,9 @@ fn scheduler_wake_and_ready_order_is_deterministic() {
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => panic!("imm size mismatch for {:?}: need {}, got {}", op, n, bytes.len()),
(n, Some(bytes)) => {
panic!("imm size mismatch for {:?}: need {}, got {}", op, n, bytes.len())
}
(n, None) => panic!("missing imm for {:?}: need {} bytes", op, n),
}
}

View File

@ -1,7 +1,7 @@
use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
use prometeu_bytecode::Value;
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_vm::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
use prometeu_bytecode::Value;
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
out.extend_from_slice(&(op as u16).to_le_bytes());
@ -9,7 +9,9 @@ fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => panic!("imm size mismatch for {:?}: need {}, got {}", op, n, bytes.len()),
(n, Some(bytes)) => {
panic!("imm size mismatch for {:?}: need {}, got {}", op, n, bytes.len())
}
(n, None) => panic!("missing imm for {:?}: need {} bytes", op, n),
}
}

View File

@ -7,11 +7,9 @@ const FORBIDDEN_IDENT_TOKENS: &[&str] = &[
// HIP must be fully removed from code
"hip",
"HIP",
// Legacy RC API surface (exact token only)
"release",
"Release",
// Legacy scope helpers (exact tokens only)
"enter_scope",
"exit_scope",
@ -22,7 +20,6 @@ const FORBIDDEN_IDENT_TOKENS: &[&str] = &[
"borrow_scope",
"mutate_scope",
"peek_scope",
// Legacy handle/gate/retain-release naming that is almost certainly RC/HIP-related
"GateHandle",
"gate_handle",
@ -48,7 +45,8 @@ const FORBIDDEN_PATH_SEGMENTS: &[&str] = &[
#[test]
fn test_no_legacy_artifacts() {
let workspace_root = find_workspace_root().expect("Failed to locate workspace root with [workspace] Cargo.toml");
let workspace_root =
find_workspace_root().expect("Failed to locate workspace root with [workspace] Cargo.toml");
// Collect Rust files under crates/**/src/**/*.rs and any build.rs under crates/**
let files = collect_rust_files(&workspace_root);
@ -70,14 +68,21 @@ fn test_no_legacy_artifacts() {
for (tok, line, col) in toks {
if is_forbidden_ident(&tok) {
violations.insert(format!("{}:{}:{} identifier '{}': legacy token",
rel(&workspace_root, path), line, col, tok));
violations.insert(format!(
"{}:{}:{} identifier '{}': legacy token",
rel(&workspace_root, path),
line,
col,
tok
));
}
}
}
if !violations.is_empty() {
let mut msg = String::from("Legacy artifacts detected (RC/HIP/scope helpers). Please remove or rename.\n");
let mut msg = String::from(
"Legacy artifacts detected (RC/HIP/scope helpers). Please remove or rename.\n",
);
for v in &violations {
msg.push_str(" - ");
msg.push_str(v);
@ -110,15 +115,18 @@ fn find_workspace_root() -> Option<PathBuf> {
fn collect_rust_files(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let crates_dir = root.join("crates");
if !crates_dir.exists() { return out; }
if !crates_dir.exists() {
return out;
}
let mut stack = vec![crates_dir];
while let Some(dir) = stack.pop() {
// Exclude noisy/non-code directories early
let name_lc = dir.file_name().and_then(|s| s.to_str()).unwrap_or("").to_ascii_lowercase();
if matches!(name_lc.as_str(),
"target" | "docs" | "files" | "sdcard" | "test-cartridges" | "temp")
|| name_lc.starts_with("dist")
if matches!(
name_lc.as_str(),
"target" | "docs" | "files" | "sdcard" | "test-cartridges" | "temp"
) || name_lc.starts_with("dist")
{
continue;
}
@ -143,10 +151,8 @@ fn collect_rust_files(root: &Path) -> Vec<PathBuf> {
}
fn path_segment_violation(path: &Path) -> Option<String> {
let mut segs: Vec<String> = path
.components()
.filter_map(|c| c.as_os_str().to_str().map(|s| s.to_string()))
.collect();
let mut segs: Vec<String> =
path.components().filter_map(|c| c.as_os_str().to_str().map(|s| s.to_string())).collect();
if let Some(fname) = path.file_stem().and_then(|s| s.to_str()) {
segs.push(fname.to_string());
}
@ -154,10 +160,7 @@ fn path_segment_violation(path: &Path) -> Option<String> {
let seg_lc = seg.to_ascii_lowercase();
for &bad in FORBIDDEN_PATH_SEGMENTS {
if seg_lc == bad.to_ascii_lowercase() {
return Some(format!(
"{} path-segment '{}'",
path.display(), seg
));
return Some(format!("{} path-segment '{}'", path.display(), seg));
}
}
}
@ -188,15 +191,22 @@ fn tokenize_identifiers(text: &str) -> Vec<(String, usize, usize)> {
let start_line = line;
let start_col = col;
let start = i;
i += 1; col += 1;
i += 1;
col += 1;
while i < bytes.len() {
let c = bytes[i] as char;
if is_ident_part(c) { i += 1; col += 1; } else { break; }
if is_ident_part(c) {
i += 1;
col += 1;
} else {
break;
}
}
let tok = &text[start..i];
out.push((tok.to_string(), start_line, start_col));
} else {
i += 1; col += 1;
i += 1;
col += 1;
}
}
out
@ -221,14 +231,20 @@ fn strip_comments_and_strings(src: &str) -> String {
while i < b.len() {
let c = b[i] as char;
// Preserve newlines to maintain line numbers
if c == '\n' { out.push('\n'); i += 1; continue; }
if c == '\n' {
out.push('\n');
i += 1;
continue;
}
// Try to match line comment
if c == '/' && i + 1 < b.len() && b[i + 1] as char == '/' {
i += 2;
while i < b.len() {
let ch = b[i] as char;
if ch == '\n' { break; }
if ch == '\n' {
break;
}
i += 1;
}
continue; // newline is handled at top on next loop
@ -239,8 +255,13 @@ fn strip_comments_and_strings(src: &str) -> String {
i += 2;
while i + 1 < b.len() {
let ch = b[i] as char;
if ch == '\n' { out.push('\n'); }
if ch == '*' && b[i + 1] as char == '/' { i += 2; break; }
if ch == '\n' {
out.push('\n');
}
if ch == '*' && b[i + 1] as char == '/' {
i += 2;
break;
}
i += 1;
}
continue;
@ -251,7 +272,10 @@ fn strip_comments_and_strings(src: &str) -> String {
let mut j = i + 1;
let mut hashes = 0usize;
if j < b.len() && b[j] as char == '#' {
while j < b.len() && b[j] as char == '#' && hashes < 10 { hashes += 1; j += 1; }
while j < b.len() && b[j] as char == '#' && hashes < 10 {
hashes += 1;
j += 1;
}
}
if j < b.len() && b[j] as char == '"' {
// Found start of raw string
@ -259,13 +283,18 @@ fn strip_comments_and_strings(src: &str) -> String {
let mut end_found = false;
while j < b.len() {
let ch = b[j] as char;
if ch == '\n' { out.push('\n'); j += 1; continue; }
if ch == '\n' {
out.push('\n');
j += 1;
continue;
}
if ch == '"' {
// check for closing hashes
let mut k = j + 1;
let mut matched = 0usize;
while matched < hashes && k < b.len() && b[k] as char == '#' {
matched += 1; k += 1;
matched += 1;
k += 1;
}
if matched == hashes {
i = k; // consume entire raw string
@ -278,7 +307,9 @@ fn strip_comments_and_strings(src: &str) -> String {
}
j += 1;
}
if !end_found { i = j; } // EOF inside string
if !end_found {
i = j;
} // EOF inside string
continue;
}
}
@ -288,12 +319,17 @@ fn strip_comments_and_strings(src: &str) -> String {
i += 1; // skip starting quote
while i < b.len() {
let ch = b[i] as char;
if ch == '\n' { out.push('\n'); }
if ch == '\n' {
out.push('\n');
}
if ch == '\\' {
i += 2; // skip escaped char
continue;
}
if ch == '"' { i += 1; break; }
if ch == '"' {
i += 1;
break;
}
i += 1;
}
continue;
@ -320,22 +356,38 @@ mod pathdiff {
// pop common prefix
let mut comps_a: Vec<Component> = Vec::new();
let mut comps_b: Vec<Component> = Vec::new();
for c in ita { comps_a.push(c); }
for c in itb { comps_b.push(c); }
for c in ita {
comps_a.push(c);
}
for c in itb {
comps_b.push(c);
}
let mut i = 0usize;
while i < comps_a.len() && i < comps_b.len() && comps_a[i] == comps_b[i] { i += 1; }
while i < comps_a.len() && i < comps_b.len() && comps_a[i] == comps_b[i] {
i += 1;
}
let mut result = PathBuf::new();
for _ in i..comps_a.len() { result.push(".."); }
for c in &comps_b[i..] { result.push(c.as_os_str()); }
for _ in i..comps_a.len() {
result.push("..");
}
for c in &comps_b[i..] {
result.push(c.as_os_str());
}
Some(result)
}
trait Absolutize { fn absolutize(&self) -> PathBuf; }
trait Absolutize {
fn absolutize(&self) -> PathBuf;
}
impl Absolutize for Path {
fn absolutize(&self) -> PathBuf {
if self.is_absolute() { self.to_path_buf() } else { std::env::current_dir().unwrap().join(self) }
if self.is_absolute() {
self.to_path_buf()
} else {
std::env::current_dir().unwrap().join(self)
}
}
}
}

View File

@ -1,10 +1,14 @@
use anyhow::Result;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
use prometeu_bytecode::model::{
BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta,
};
use std::fs;
use std::path::PathBuf;
fn asm(s: &str) -> Vec<u8> { assemble(s).expect("assemble") }
fn asm(s: &str) -> Vec<u8> {
assemble(s).expect("assemble")
}
pub fn generate() -> Result<()> {
let mut rom: Vec<u8> = Vec::new();
@ -12,16 +16,14 @@ pub fn generate() -> Result<()> {
heavy_load(&mut rom);
// light_load(&mut rom);
let functions = vec![
FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
param_slots: 0,
local_slots: 2,
return_slots: 0,
max_stack_slots: 16,
},
];
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
param_slots: 0,
local_slots: 2,
return_slots: 0,
max_stack_slots: 16,
}];
let module = BytecodeModule {
version: 0,
@ -72,7 +74,7 @@ fn heavy_load(mut rom: &mut Vec<u8>) {
// draw 100 discs using t for animation
// draw 20 texts using t for animation
// RET (runtime handles the frame loop)
// --- init locals ---
// local 0: scratch
// local 1: loop counter for discs
@ -119,7 +121,9 @@ fn heavy_load(mut rom: &mut Vec<u8>) {
rom.extend(asm("JMP_IF_FALSE 0"));
// x = (t * 3 + i * 40) % 320
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD"));
rom.extend(asm(
"GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD",
));
// y = (i * 30 + t) % 180
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 30\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 180\nMOD"));
// string (toggle between "stress" and "frame")

View File

@ -1,3 +1,5 @@
use anyhow::Result;
fn main() -> Result<()> { pbxgen_stress::generate() }
fn main() -> Result<()> {
pbxgen_stress::generate()
}

View File

@ -0,0 +1,324 @@
# PR-2 - PBX SYSC and HOSTCALL Loader Patching
## Goal
Teach the runtime to load canonical host bindings from PBX, resolve them at load time, validate ABI and capabilities, and rewrite pre-load host calls into final numeric syscalls.
This PR assumes PR-1 is already available, so cartridge capabilities are exposed to the loader as internal `CapFlags`.
## Why
Prometeu's hardware contract is:
- source- and SDK-level host APIs map to canonical identities `(module, name, version)`
- load-time resolution maps those identities to numeric syscall ids
- runtime execution is numeric-only via `SYSCALL <id>`
The runtime already has the important building blocks:
- canonical syscall registry
- load-time `resolve_program_syscalls`
- verifier support for numeric `SYSCALL <id>`
- VM dispatch by numeric id
What is missing is the PBX and loader wiring.
## Scope
In scope:
- add a mandatory PBX `SYSC` section
- extend each `SYSC` entry with ABI shape
- add pre-load opcode `HOSTCALL <sysc_index>`
- parse `SYSC` at load time
- resolve `SYSC` against the host syscall registry
- validate declared ABI against authoritative host metadata
- validate capabilities using cartridge-granted `CapFlags`
- reject unused `SYSC` entries
- reject out-of-bounds `HOSTCALL` indices
- patch all `HOSTCALL <sysc_index>` to `SYSCALL <id>`
- ensure verifier runs only on the patched image
Out of scope:
- compiler emission
- stdlib SDK pack format
- final external tooling for PBX generation
- platform policy beyond current manifest-derived granted capabilities
## Architectural Contract
### PBX `SYSC`
`SYSC` is a unique, deduplicated, program-wide table of declared host bindings.
Each entry carries:
- `module`
- `name`
- `version`
- `arg_slots`
- `ret_slots`
`SYSC` is mandatory for every valid PBX.
If the program requires no host bindings, `SYSC` is present with `count = 0`.
### Pre-load callsites
The compiler-side artifact form is:
```text
HOSTCALL <u32 sysc_index>
```
Rules:
- `sysc_index` is zero-based into the `SYSC` table
- code must not contain final `SYSCALL <id>` for unresolved host-backed SDK calls
- the final executable image given to the VM must contain no `HOSTCALL`
### Load-time responsibilities
The loader must:
1. parse `SYSC`
2. resolve each entry to `syscall_id`
3. validate `arg_slots` and `ret_slots`
4. validate capabilities
5. scan code for `HOSTCALL`
6. mark used `SYSC` indices
7. reject out-of-bounds indices
8. reject unused `SYSC` entries
9. rewrite `HOSTCALL <index>` into `SYSCALL <id>`
10. ensure no `HOSTCALL` remains
11. only then hand the image to the verifier
## PBX Format Changes
### `prometeu-bytecode`
Add a new section kind:
- `SYSC`
Recommended temporary binary layout:
```text
u32 count
repeat count times:
u16 module_len
[module_len bytes UTF-8]
u16 name_len
[name_len bytes UTF-8]
u16 version
u16 arg_slots
u16 ret_slots
```
Rules:
- strings are UTF-8
- duplicate canonical identities are invalid
- malformed lengths are invalid
- missing `SYSC` is invalid
### Bytecode model changes
Extend `BytecodeModule` with a syscall declaration vector.
Suggested shape:
```rust
pub struct SyscallDecl {
pub module: String,
pub name: String,
pub version: u16,
pub arg_slots: u16,
pub ret_slots: u16,
}
```
Then:
```rust
pub syscalls: Vec<SyscallDecl>
```
Serialization/deserialization must include section kind for `SYSC`.
## Opcode Changes
### Add `HOSTCALL`
Add a new opcode:
- `HOSTCALL`
Immediate:
- `u32 sysc_index`
Recommended behavior:
- valid only in pre-load artifact form
- not allowed to reach final verifier/VM execution path
Required code updates:
- opcode enum
- decoder
- opcode spec
- assembler
- disassembler
Verifier recommendation:
- the verifier does not need to support `HOSTCALL`
- it should continue to run after loader patching only
## Loader Integration
### Resolution
Use the existing canonical resolver in `prometeu-hal::syscalls`.
For each `SYSC` entry:
1. resolve `(module, name, version)`
2. obtain `SyscallResolved`
3. compare `arg_slots`
4. compare `ret_slots`
5. compare required capability against granted cartridge flags
If any check fails, load fails deterministically.
### Capability source
Capabilities come from the already-loaded cartridge manifest via PR-1.
This PR must not invent authority from PBX contents.
### Code patching
Recommended algorithm:
1. parse code buffer
2. whenever `HOSTCALL <index>` is found:
3. validate `index < syscalls.len()`
4. mark that `SYSC[index]` is used
5. rewrite opcode/immediate in place or rebuild the code buffer
6. emit final `SYSCALL <resolved_id>`
After scan:
1. every `SYSC` entry must be marked used
2. no `HOSTCALL` may remain
Either patching strategy is acceptable:
- mutate byte buffer in place
- rebuild a fresh patched buffer
The final `ProgramImage` must contain only numeric `SYSCALL <id>`.
## Verifier Contract
No verifier redesign is required.
The intended contract is:
- loader validates interface compatibility
- verifier validates final numeric program structure
This means:
- loader checks `SYSC`-declared ABI against host metadata
- verifier checks stack effects of final `SYSCALL <id>` using existing runtime metadata
This is intentional and not considered harmful duplication.
## Proposed Code Areas
### `prometeu-bytecode`
Likely files:
- `src/model.rs`
- `src/opcode.rs`
- `src/opcode_spec.rs`
- `src/decoder.rs`
- `src/assembler.rs`
- `src/disassembler.rs`
- `src/lib.rs`
### `prometeu-hal`
Likely files:
- `src/cartridge_loader.rs`
- possibly helper types or resolver glue near syscall loading logic
### `prometeu-vm`
Likely files:
- load path in `src/virtual_machine.rs`
Required behavior:
- patch before `Verifier::verify(...)`
## Deterministic Load Errors
Load must fail for at least:
1. missing `SYSC`
2. malformed `SYSC`
3. invalid UTF-8
4. duplicate syscall identities
5. unknown syscall identity
6. ABI mismatch between `SYSC` and host metadata
7. missing capability
8. `HOSTCALL` with out-of-bounds `sysc_index`
9. declared `SYSC` entry unused by all `HOSTCALL`s
10. `HOSTCALL` still present after patch
## Acceptance Criteria
- PBX parser supports mandatory `SYSC`
- `BytecodeModule` carries syscall declarations
- runtime understands `HOSTCALL`
- loader resolves `SYSC` entries before verification
- loader validates `arg_slots` and `ret_slots`
- loader validates capabilities against cartridge flags
- loader rewrites `HOSTCALL` to `SYSCALL`
- verifier runs only on patched code
- final VM execution path remains numeric-only
## Tests
Add tests covering:
1. valid PBX with empty `SYSC` and no `HOSTCALL`
2. valid PBX with one syscall and one `HOSTCALL`
3. unknown syscall identity
4. capability mismatch
5. ABI mismatch
6. missing `SYSC`
7. duplicate `SYSC` entries
8. malformed `SYSC` payload
9. `HOSTCALL` index out of bounds
10. unused `SYSC` entry
11. patched image contains only `SYSCALL`
Prefer synthetic in-memory PBX images in tests.
## Definition of Done
After this PR:
- PBX declares canonical host bindings in `SYSC`
- pre-load code references those bindings with `HOSTCALL`
- the loader resolves and validates them during load
- the loader patches executable code to `SYSCALL <id>`
- the verifier and VM continue to operate on numeric syscalls only