adjustments on capabilities on cartridges
This commit is contained in:
parent
cfdca93160
commit
d5ef8a2003
@ -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;
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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,9 +75,7 @@ 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| {
|
||||
let idx = match self.sleeping.binary_search_by(|e| {
|
||||
if e.wake_tick == entry.wake_tick {
|
||||
e.seq.cmp(&entry.seq)
|
||||
} else {
|
||||
@ -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() {
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
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,
|
||||
@ -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")
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> { pbxgen_stress::generate() }
|
||||
fn main() -> Result<()> {
|
||||
pbxgen_stress::generate()
|
||||
}
|
||||
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user