diff --git a/crates/console/prometeu-hal/src/syscalls.rs b/crates/console/prometeu-hal/src/syscalls.rs index 0b244f56..aec43b61 100644 --- a/crates/console/prometeu-hal/src/syscalls.rs +++ b/crates/console/prometeu-hal/src/syscalls.rs @@ -237,6 +237,44 @@ pub enum LoadError { }, } +/// Load-time error for PBX-declared syscall resolution. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeclaredLoadError { + /// The `(module, name, version)` triple is not known by the host. + UnknownSyscall { + module: String, + name: String, + version: u16, + }, + /// The cartridge lacks required capabilities for the syscall. + MissingCapability { + required: CapFlags, + provided: CapFlags, + module: String, + name: String, + version: u16, + }, + /// The PBX-declared ABI does not match the authoritative host metadata. + AbiMismatch { + module: String, + name: String, + version: u16, + declared_arg_slots: u16, + declared_ret_slots: u16, + expected_arg_slots: u16, + expected_ret_slots: u16, + }, +} + +fn resolve_syscall_impl(module: &str, name: &str, version: u16) -> Option { + for entry in SYSCALL_TABLE { + if entry.meta.module == module && entry.meta.name == name && entry.meta.version == version { + return Some(SyscallResolved { id: entry.meta.id, meta: entry.meta }); + } + } + None +} + /// Resolve a canonical syscall identity to its numeric id and metadata. /// /// Returns `None` if the identity is unknown. @@ -245,12 +283,7 @@ pub fn resolve_syscall( name: &'static str, version: u16, ) -> Option { - for entry in SYSCALL_TABLE { - if entry.meta.module == module && entry.meta.name == name && entry.meta.version == version { - return Some(SyscallResolved { id: entry.meta.id, meta: entry.meta }); - } - } - None + resolve_syscall_impl(module, name, version) } /// Resolve all declared program syscalls and enforce capability gating. @@ -288,6 +321,58 @@ pub fn resolve_program_syscalls( Ok(out) } +/// Resolve and validate syscall declarations carried by a PBX module. +/// +/// This is the loader-facing path for `BytecodeModule.syscalls`: +/// - canonical identity must exist in the host table +/// - capability requirements must be satisfied by granted cart flags +/// - declared ABI must match authoritative host metadata +pub fn resolve_declared_program_syscalls( + declared: &[prometeu_bytecode::SyscallDecl], + caps: CapFlags, +) -> Result, DeclaredLoadError> { + let mut out = Vec::with_capacity(declared.len()); + + for decl in declared { + let Some(res) = resolve_syscall_impl(&decl.module, &decl.name, decl.version) else { + return Err(DeclaredLoadError::UnknownSyscall { + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + }); + }; + + let missing = res.meta.caps & !caps; + if missing != 0 { + return Err(DeclaredLoadError::MissingCapability { + required: res.meta.caps, + provided: caps, + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + }); + } + + let expected_arg_slots = u16::from(res.meta.arg_slots); + let expected_ret_slots = res.meta.ret_slots; + if decl.arg_slots != expected_arg_slots || decl.ret_slots != expected_ret_slots { + return Err(DeclaredLoadError::AbiMismatch { + module: decl.module.clone(), + name: decl.name.clone(), + version: decl.version, + declared_arg_slots: decl.arg_slots, + declared_ret_slots: decl.ret_slots, + expected_arg_slots, + expected_ret_slots, + }); + } + + out.push(res); + } + + Ok(out) +} + /// Canonical registry of all syscalls and their metadata. /// /// IMPORTANT: This table is the single authoritative source for the slot-based @@ -1394,4 +1479,89 @@ mod tests { assert_eq!(ok.len(), 1); assert_eq!(ok[0].id, 0x1001); } + + #[test] + fn declared_resolver_returns_expected_id_for_known_identity() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }]; + + let ok = + resolve_declared_program_syscalls(&declared, caps::GFX).expect("must resolve with ABI"); + assert_eq!(ok.len(), 1); + assert_eq!(ok[0].id, 0x1001); + } + + #[test] + fn declared_resolver_rejects_unknown_identity() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "nonexistent".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }]; + + let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::UnknownSyscall { + module: "gfx".into(), + name: "nonexistent".into(), + version: 1, + } + ); + } + + #[test] + fn declared_resolver_rejects_missing_capability() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }]; + + let err = resolve_declared_program_syscalls(&declared, caps::NONE).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::MissingCapability { + required: caps::GFX, + provided: caps::NONE, + module: "gfx".into(), + name: "clear".into(), + version: 1, + } + ); + } + + #[test] + fn declared_resolver_rejects_abi_mismatch() { + let declared = [prometeu_bytecode::SyscallDecl { + module: "gfx".into(), + name: "draw_line".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }]; + + let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err(); + assert_eq!( + err, + DeclaredLoadError::AbiMismatch { + module: "gfx".into(), + name: "draw_line".into(), + version: 1, + declared_arg_slots: 4, + declared_ret_slots: 0, + expected_arg_slots: 5, + expected_ret_slots: 0, + } + ); + } }