diff --git a/crates/console/prometeu-bytecode/src/assembler.rs b/crates/console/prometeu-bytecode/src/assembler.rs index 8b8d2cce..3a8ba823 100644 --- a/crates/console/prometeu-bytecode/src/assembler.rs +++ b/crates/console/prometeu-bytecode/src/assembler.rs @@ -184,7 +184,7 @@ pub fn assemble(src: &str) -> Result, AsmError> { emit_u16(CoreOpCode::FrameSync as u16, &mut out); } - // One u32 immediate (decimal or hex accepted for SYSCALL only; others decimal ok) + // One u32 immediate (decimal or hex accepted; SYSCALL/HOSTCALL commonly use hex/idx) "JMP" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); @@ -338,6 +338,13 @@ pub fn assemble(src: &str) -> Result, AsmError> { emit_u16(CoreOpCode::Sleep as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out); } + "HOSTCALL" => { + if ops.is_empty() { + return Err(AsmError::MissingOperand(line.into())); + } + emit_u16(CoreOpCode::Hostcall as u16, &mut out); + emit_u32(parse_u32_any(ops)?, &mut out); + } "SYSCALL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); diff --git a/crates/console/prometeu-bytecode/src/disassembler.rs b/crates/console/prometeu-bytecode/src/disassembler.rs index 6d82c210..06728925 100644 --- a/crates/console/prometeu-bytecode/src/disassembler.rs +++ b/crates/console/prometeu-bytecode/src/disassembler.rs @@ -13,6 +13,7 @@ //! - `MAKE_CLOSURE fn=, captures=` //! - `SPAWN fn=, argc=` //! - `CALL_CLOSURE argc=` +//! - `HOSTCALL ` is printed in decimal because it is a `SYSC` table index. //! - `SYSCALL` is printed as `SYSCALL 0xhhhh` (numeric id in hex) to avoid cross-crate deps. //! //! Notes: @@ -58,7 +59,8 @@ fn format_operand(op: CoreOpCode, imm: &[u8]) -> String { | CoreOpCode::GetLocal | CoreOpCode::SetLocal | CoreOpCode::Call - | CoreOpCode::Sleep => { + | CoreOpCode::Sleep + | CoreOpCode::Hostcall => { let v = u32::from_le_bytes(imm.try_into().unwrap()); format!("{}", v) } diff --git a/crates/console/prometeu-bytecode/src/model.rs b/crates/console/prometeu-bytecode/src/model.rs index aabffc01..06dd53a0 100644 --- a/crates/console/prometeu-bytecode/src/model.rs +++ b/crates/console/prometeu-bytecode/src/model.rs @@ -679,6 +679,7 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> { | OpCode::GetLocal | OpCode::SetLocal | OpCode::PopN + | OpCode::Hostcall | OpCode::Syscall => { pos += 4; } diff --git a/crates/console/prometeu-bytecode/src/opcode.rs b/crates/console/prometeu-bytecode/src/opcode.rs index 96d4e0b7..e286f058 100644 --- a/crates/console/prometeu-bytecode/src/opcode.rs +++ b/crates/console/prometeu-bytecode/src/opcode.rs @@ -185,6 +185,11 @@ pub enum OpCode { Sleep = 0x56, // --- 6.8 Peripherals and System --- + /// Pre-load host binding call by `SYSC` table index. + /// Operand: sysc_index (u32) + /// This opcode is valid only in PBX artifact form and must be patched by the loader + /// into a final numeric `SYSCALL ` before verification or execution. + Hostcall = 0x71, /// Invokes a system function (Firmware/OS). /// Operand: syscall_id (u32) /// Stack: [args...] -> [results...] (depends on syscall) @@ -246,6 +251,7 @@ impl TryFrom for OpCode { 0x55 => Ok(OpCode::Yield), 0x56 => Ok(OpCode::Sleep), 0x70 => Ok(OpCode::Syscall), + 0x71 => Ok(OpCode::Hostcall), 0x80 => Ok(OpCode::FrameSync), _ => Err(format!("Invalid OpCode: 0x{:04X}", value)), } @@ -304,6 +310,7 @@ impl OpCode { OpCode::Yield => 1, OpCode::Sleep => 1, OpCode::Syscall => 1, + OpCode::Hostcall => 1, OpCode::FrameSync => 1, } } diff --git a/crates/console/prometeu-bytecode/src/opcode_spec.rs b/crates/console/prometeu-bytecode/src/opcode_spec.rs index 2d68c2d3..8bbc16af 100644 --- a/crates/console/prometeu-bytecode/src/opcode_spec.rs +++ b/crates/console/prometeu-bytecode/src/opcode_spec.rs @@ -493,6 +493,16 @@ impl OpCodeSpecExt for OpCode { // Considered a safepoint since it forces a frame boundary is_safepoint: true, }, + OpCode::Hostcall => OpcodeSpec { + name: "HOSTCALL", + imm_bytes: 4, + pops: 0, + pushes: 0, + is_branch: false, + is_terminator: false, + may_trap: false, + is_safepoint: false, + }, OpCode::Syscall => OpcodeSpec { name: "SYSCALL", imm_bytes: 4, diff --git a/crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs b/crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs index 4dcb6fb0..7199f960 100644 --- a/crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs +++ b/crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs @@ -11,7 +11,7 @@ fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec) { #[test] fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() { // Program: PUSH_I32 7; MAKE_CLOSURE fn=1,captures=0; CALL_CLOSURE argc=1; - // SPAWN fn=2,argc=1; YIELD; SLEEP 3; SYSCALL 0x1003; FRAME_SYNC; HALT + // SPAWN fn=2,argc=1; YIELD; SLEEP 3; HOSTCALL 2; SYSCALL 0x1003; FRAME_SYNC; HALT let mut prog = Vec::new(); emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog); @@ -31,6 +31,8 @@ fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() { emit(CoreOpCode::Yield, None, &mut prog); // SLEEP 3 emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog); + // HOSTCALL sysc[2] + emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog); // SYSCALL gfx.draw_line (0x1003) emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog); // FRAME_SYNC diff --git a/crates/console/prometeu-bytecode/tests/disasm_snapshot.rs b/crates/console/prometeu-bytecode/tests/disasm_snapshot.rs index cf2341f1..a3141a49 100644 --- a/crates/console/prometeu-bytecode/tests/disasm_snapshot.rs +++ b/crates/console/prometeu-bytecode/tests/disasm_snapshot.rs @@ -28,6 +28,8 @@ fn snapshot_representative_program_is_stable() { emit(CoreOpCode::Yield, None, &mut prog); // SLEEP 3 emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog); + // HOSTCALL 2 + emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog); // SYSCALL 0x1003 emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog); // FRAME_SYNC @@ -36,6 +38,6 @@ fn snapshot_representative_program_is_stable() { emit(CoreOpCode::Halt, None, &mut prog); let text = disassemble(&prog).expect("disasm ok"); - let expected = "PUSH_I32 7\nMAKE_CLOSURE fn=1, captures=0\nCALL_CLOSURE argc=1\nSPAWN fn=2, argc=1\nYIELD\nSLEEP 3\nSYSCALL 0x1003\nFRAME_SYNC\nHALT"; + let expected = "PUSH_I32 7\nMAKE_CLOSURE fn=1, captures=0\nCALL_CLOSURE argc=1\nSPAWN fn=2, argc=1\nYIELD\nSLEEP 3\nHOSTCALL 2\nSYSCALL 0x1003\nFRAME_SYNC\nHALT"; assert_eq!(text, expected); } diff --git a/crates/console/prometeu-bytecode/tests/roundtrip.rs b/crates/console/prometeu-bytecode/tests/roundtrip.rs index 3755cdf6..7bfe7bf7 100644 --- a/crates/console/prometeu-bytecode/tests/roundtrip.rs +++ b/crates/console/prometeu-bytecode/tests/roundtrip.rs @@ -39,7 +39,7 @@ fn disasm(bytes: &[u8]) -> String { CoreOpCode::PushF64 => format!("{}", instr.imm_f64().unwrap()), CoreOpCode::PushBool => format!("{}", instr.imm_u8().unwrap()), CoreOpCode::PushI32 => format!("{}", instr.imm_i32().unwrap()), - CoreOpCode::PopN | CoreOpCode::PushConst => { + CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => { format!("{}", instr.imm_u32().unwrap()) } _ => format!("0x{}", hex::encode(instr.imm)), @@ -115,6 +115,20 @@ fn disasm_contains_expected_mnemonics_and_operands() { assert!(text.contains("HALT")); } +#[test] +fn hostcall_roundtrips_with_decimal_index() { + let mut prog = Vec::new(); + prog.extend(encode_instr(CoreOpCode::Hostcall, Some(&7u32.to_le_bytes()))); + prog.extend(encode_instr(CoreOpCode::Halt, None)); + + let text = disasm(&prog); + + assert!(text.contains("HOSTCALL 7")); + + let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble hostcall"); + assert_eq!(rebuilt, prog); +} + // Minimal hex helper to avoid extra deps in tests mod hex { pub fn encode(bytes: &[u8]) -> String { diff --git a/crates/console/prometeu-vm/src/verifier.rs b/crates/console/prometeu-vm/src/verifier.rs index 129ccb00..655f3fe5 100644 --- a/crates/console/prometeu-vm/src/verifier.rs +++ b/crates/console/prometeu-vm/src/verifier.rs @@ -67,6 +67,10 @@ pub enum VerifierError { pc: usize, id: u32, }, + HostcallNotPatched { + pc: usize, + sysc_index: u32, + }, /// Execution can fall through past the end of the function without a valid terminator /// (e.g., RET, JMP to end, HALT/TRAP). Verifier requires every reachable path to end /// in a terminator. @@ -254,6 +258,12 @@ impl Verifier { (arg_count, 0) } OpCode::Ret => (func.return_slots, 0), + OpCode::Hostcall => { + return Err(VerifierError::HostcallNotPatched { + pc: func_start + pc, + sysc_index: instr.imm_u32().unwrap(), + }); + } OpCode::Syscall => { let id = instr.imm_u32().unwrap(); let syscall = Syscall::from_u32(id).ok_or_else(|| { @@ -1145,6 +1155,18 @@ mod tests { assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF })); } + #[test] + fn test_verifier_rejects_unpatched_hostcall() { + let mut code = Vec::new(); + code.push(OpCode::Hostcall as u8); + code.push(0x00); + code.extend_from_slice(&3u32.to_le_bytes()); + + let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }]; + let res = Verifier::verify(&code, &functions); + assert_eq!(res, Err(VerifierError::HostcallNotPatched { pc: 0, sysc_index: 3 })); + } + #[test] fn test_function_without_terminator_is_rejected() { // Single NOP with no RET/JMP/TRAP/HALT at the end → fallthrough to end diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index d93064b5..77f6dffd 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -699,6 +699,15 @@ impl VirtualMachine { self.handle_safepoint(); return Err(LogicalFrameEndingReason::FrameSync); } + OpCode::Hostcall => { + let sysc_index = instr + .imm_u32() + .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; + return Err(LogicalFrameEndingReason::Panic(format!( + "HOSTCALL {} reached execution without loader patching", + sysc_index + ))); + } OpCode::MakeClosure => { // Immediate carries (fn_id, capture_count) let (fn_id, cap_count) = instr @@ -2639,9 +2648,16 @@ mod tests { #[test] fn test_loader_hardening_entrypoint_not_found() { let mut vm = VirtualMachine::default(); - // Valid empty PBS v0 module - let mut header = vec![0u8; 32]; - header[0..4].copy_from_slice(b"PBS\0"); + let header = prometeu_bytecode::model::BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![], + code: vec![], + debug_info: None, + exports: vec![], + syscalls: vec![], + } + .serialize(); // Try to initialize with numeric entrypoint 10 (out of bounds for empty ROM) let res = vm.initialize(header, "10"); @@ -2657,8 +2673,16 @@ mod tests { let mut vm = VirtualMachine::default(); vm.pc = 123; // Pollution - let mut header = vec![0u8; 32]; - header[0..4].copy_from_slice(b"PBS\0"); + let header = prometeu_bytecode::model::BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![], + code: vec![], + debug_info: None, + exports: vec![], + syscalls: vec![], + } + .serialize(); let res = vm.initialize(header, ""); assert!(res.is_ok());