diff --git a/crates/console/prometeu-bytecode/src/abi.rs b/crates/console/prometeu-bytecode/src/abi.rs index d0d76f98..311530af 100644 --- a/crates/console/prometeu-bytecode/src/abi.rs +++ b/crates/console/prometeu-bytecode/src/abi.rs @@ -19,6 +19,8 @@ pub const TRAP_DIV_ZERO: u32 = 0x0000_000A; pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B; /// Executed RET with an incorrect stack height (mismatch with function metadata). pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C; +/// The intrinsic ID provided is not recognized by the runtime, or its metadata is invalid. +pub const TRAP_INVALID_INTRINSIC: u32 = 0x0000_000D; use serde::{Deserialize, Serialize}; diff --git a/crates/console/prometeu-bytecode/src/assembler.rs b/crates/console/prometeu-bytecode/src/assembler.rs index 3a8ba823..dd988694 100644 --- a/crates/console/prometeu-bytecode/src/assembler.rs +++ b/crates/console/prometeu-bytecode/src/assembler.rs @@ -352,6 +352,13 @@ pub fn assemble(src: &str) -> Result, AsmError> { emit_u16(CoreOpCode::Syscall as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out); } + "INTRINSIC" => { + if ops.is_empty() { + return Err(AsmError::MissingOperand(line.into())); + } + emit_u16(CoreOpCode::Intrinsic as u16, &mut out); + emit_u32(parse_u32_any(ops)?, &mut out); + } other => return Err(AsmError::UnknownMnemonic(other.into())), } diff --git a/crates/console/prometeu-bytecode/src/disassembler.rs b/crates/console/prometeu-bytecode/src/disassembler.rs index 06728925..2aedf2c2 100644 --- a/crates/console/prometeu-bytecode/src/disassembler.rs +++ b/crates/console/prometeu-bytecode/src/disassembler.rs @@ -83,6 +83,10 @@ fn format_operand(op: CoreOpCode, imm: &[u8]) -> String { // Hex id stable, avoids dependency on HAL metadata. format!("0x{:04x}", id) } + CoreOpCode::Intrinsic => { + let id = u32::from_le_bytes(imm.try_into().unwrap()); + format!("0x{:04x}", id) + } _ => { // Fallback: raw immediate hex (little-endian, as encoded) let mut s = String::with_capacity(2 + imm.len() * 2); diff --git a/crates/console/prometeu-bytecode/src/lib.rs b/crates/console/prometeu-bytecode/src/lib.rs index 3a936e7a..fce85565 100644 --- a/crates/console/prometeu-bytecode/src/lib.rs +++ b/crates/console/prometeu-bytecode/src/lib.rs @@ -12,7 +12,8 @@ mod value; 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, + TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, + TRAP_STACK_UNDERFLOW, TRAP_TYPE, }; pub use assembler::{assemble, AsmError}; pub use decoder::{decode_next, DecodeError}; diff --git a/crates/console/prometeu-bytecode/src/model.rs b/crates/console/prometeu-bytecode/src/model.rs index 6eed73a9..31825e56 100644 --- a/crates/console/prometeu-bytecode/src/model.rs +++ b/crates/console/prometeu-bytecode/src/model.rs @@ -682,7 +682,8 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> { | OpCode::SetLocal | OpCode::PopN | OpCode::Hostcall - | OpCode::Syscall => { + | OpCode::Syscall + | OpCode::Intrinsic => { pos += 4; } OpCode::PushI64 | OpCode::PushF64 => { diff --git a/crates/console/prometeu-bytecode/src/opcode.rs b/crates/console/prometeu-bytecode/src/opcode.rs index d619a46d..9aedf2d8 100644 --- a/crates/console/prometeu-bytecode/src/opcode.rs +++ b/crates/console/prometeu-bytecode/src/opcode.rs @@ -195,6 +195,10 @@ pub enum OpCode { /// Operand: syscall_id (u32) /// Stack: [args...] -> [results...] (depends on syscall) Syscall = 0x70, + /// Invokes a VM-owned intrinsic by final numeric id. + /// Operand: intrinsic_id (u32) + /// Stack: [args...] -> [results...] (depends on intrinsic metadata) + Intrinsic = 0x72, /// Synchronizes the VM with the hardware frame (usually 60Hz). /// Execution pauses until the next VSync. FrameSync = 0x80, @@ -253,6 +257,7 @@ impl TryFrom for OpCode { 0x56 => Ok(OpCode::Sleep), 0x70 => Ok(OpCode::Syscall), 0x71 => Ok(OpCode::Hostcall), + 0x72 => Ok(OpCode::Intrinsic), 0x80 => Ok(OpCode::FrameSync), _ => Err(format!("Invalid OpCode: 0x{:04X}", value)), } @@ -312,6 +317,7 @@ impl OpCode { OpCode::Sleep => 1, OpCode::Syscall => 1, OpCode::Hostcall => 1, + OpCode::Intrinsic => 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 8bbc16af..43a49971 100644 --- a/crates/console/prometeu-bytecode/src/opcode_spec.rs +++ b/crates/console/prometeu-bytecode/src/opcode_spec.rs @@ -513,6 +513,16 @@ impl OpCodeSpecExt for OpCode { may_trap: true, is_safepoint: false, }, + OpCode::Intrinsic => OpcodeSpec { + name: "INTRINSIC", + imm_bytes: 4, + pops: 0, + pushes: 0, + is_branch: false, + is_terminator: false, + may_trap: true, + is_safepoint: false, + }, OpCode::FrameSync => OpcodeSpec { name: "FRAME_SYNC", imm_bytes: 0, diff --git a/crates/console/prometeu-bytecode/tests/roundtrip.rs b/crates/console/prometeu-bytecode/tests/roundtrip.rs index 7bfe7bf7..73fd2d59 100644 --- a/crates/console/prometeu-bytecode/tests/roundtrip.rs +++ b/crates/console/prometeu-bytecode/tests/roundtrip.rs @@ -42,6 +42,9 @@ fn disasm(bytes: &[u8]) -> String { CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => { format!("{}", instr.imm_u32().unwrap()) } + CoreOpCode::Syscall | CoreOpCode::Intrinsic => { + format!("0x{}", hex::encode(instr.imm)) + } _ => format!("0x{}", hex::encode(instr.imm)), }; line.push_str(&s); @@ -129,6 +132,20 @@ fn hostcall_roundtrips_with_decimal_index() { assert_eq!(rebuilt, prog); } +#[test] +fn intrinsic_roundtrips_with_hex_id() { + let mut prog = Vec::new(); + prog.extend(encode_instr(CoreOpCode::Intrinsic, Some(&0x1000u32.to_le_bytes()))); + prog.extend(encode_instr(CoreOpCode::Halt, None)); + + let text = prometeu_bytecode::disassemble(&prog).expect("disasm intrinsic"); + + assert!(text.contains("INTRINSIC 0x1000")); + + let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble intrinsic"); + 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/builtins.rs b/crates/console/prometeu-vm/src/builtins.rs index 797fd10d..b4f2184a 100644 --- a/crates/console/prometeu-vm/src/builtins.rs +++ b/crates/console/prometeu-vm/src/builtins.rs @@ -180,6 +180,31 @@ impl IntrinsicMeta { pub const fn key(&self) -> IntrinsicKey { IntrinsicKey::new(self.owner, self.name, self.version) } + + pub fn arg_slots(&self) -> usize { + self.arg_layout.len() + } + + pub fn ret_slots(&self) -> usize { + self.ret_layout.len() + } + + pub fn validate_result_values(&self, values: &[Value]) -> Result<(), IntrinsicExecutionError> { + if values.len() != self.ret_slots() { + return Err(IntrinsicExecutionError::ArityMismatch { + expected: self.ret_slots(), + got: values.len(), + }); + } + + for (index, (value, expected)) in values.iter().zip(self.ret_layout.iter()).enumerate() { + if !value_matches_abi_type(value, *expected) { + return Err(IntrinsicExecutionError::TypeMismatch { index, expected: *expected }); + } + } + + Ok(()) + } } const COLOR: BuiltinTypeKey = BuiltinTypeKey::new("color", 1); @@ -352,6 +377,15 @@ fn expect_float_arg(args: &[Value], index: usize) -> Result bool { + match expected { + AbiType::Scalar(BuiltinScalarType::Int) => value.as_integer().is_some(), + AbiType::Scalar(BuiltinScalarType::Float) => value.as_float().is_some(), + AbiType::Scalar(BuiltinScalarType::Bool) => matches!(value, Value::Boolean(_)), + AbiType::Builtin(_) => value.as_integer().is_some(), + } +} + fn vec2_dot(args: &[Value]) -> Result, IntrinsicExecutionError> { if args.len() != 4 { return Err(IntrinsicExecutionError::ArityMismatch { expected: 4, got: args.len() }); diff --git a/crates/console/prometeu-vm/src/verifier.rs b/crates/console/prometeu-vm/src/verifier.rs index 655f3fe5..b389f735 100644 --- a/crates/console/prometeu-vm/src/verifier.rs +++ b/crates/console/prometeu-vm/src/verifier.rs @@ -1,3 +1,4 @@ +use crate::lookup_intrinsic_by_id; use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::isa::core::CoreOpCodeSpecExt as OpCodeSpecExt; @@ -59,6 +60,10 @@ pub enum VerifierError { pc: usize, id: u32, }, + InvalidIntrinsicId { + pc: usize, + id: u32, + }, TrailingBytes { func_idx: usize, at_pc: usize, @@ -271,6 +276,13 @@ impl Verifier { })?; (syscall.args_count() as u16, syscall.results_count() as u16) } + OpCode::Intrinsic => { + let id = instr.imm_u32().unwrap(); + let intrinsic = lookup_intrinsic_by_id(id).ok_or_else(|| { + VerifierError::InvalidIntrinsicId { pc: func_start + pc, id } + })?; + (intrinsic.arg_slots() as u16, intrinsic.ret_slots() as u16) + } _ => (spec.pops, spec.pushes), }; @@ -397,6 +409,13 @@ impl Verifier { out_types.push(Unknown); } } + OpCode::Intrinsic => { + let id = instr.imm_u32().unwrap(); + let intrinsic = lookup_intrinsic_by_id(id).unwrap(); + for _ in 0..(intrinsic.ret_slots() as u16) { + out_types.push(Unknown); + } + } _ => { // Default: push Unknown for any declared _pushes if spec.pushes > 0 { @@ -414,6 +433,7 @@ impl Verifier { 0 } OpCode::Syscall => spec.pushes, // already added Unknowns above + OpCode::Intrinsic => spec.pushes, // already added Unknowns above OpCode::Call => spec.pushes, // already added Unknowns above _ => spec.pushes, }); @@ -1155,6 +1175,63 @@ mod tests { assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF })); } + #[test] + fn test_verifier_invalid_intrinsic_id() { + let mut code = Vec::new(); + code.push(OpCode::Intrinsic as u8); + code.push(0x00); + code.extend_from_slice(&0xDEADBEEFu32.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::InvalidIntrinsicId { pc: 0, id: 0xDEADBEEF })); + } + + #[test] + fn test_verifier_accepts_intrinsic_stack_effects() { + let mut code = Vec::new(); + for value in [1i32, 2, 3, 4] { + code.push(OpCode::PushI32 as u8); + code.push(0x00); + code.extend_from_slice(&value.to_le_bytes()); + } + code.push(OpCode::Intrinsic as u8); + code.push(0x00); + code.extend_from_slice(&0x1000u32.to_le_bytes()); + code.push(OpCode::Ret as u8); + code.push(0x00); + + let functions = vec![FunctionMeta { + code_offset: 0, + code_len: code.len() as u32, + return_slots: 1, + ..Default::default() + }]; + let res = Verifier::verify(&code, &functions).expect("intrinsic program must verify"); + assert!(res[0] >= 4); + } + + #[test] + fn test_verifier_rejects_intrinsic_stack_underflow() { + let mut code = Vec::new(); + code.push(OpCode::PushI32 as u8); + code.push(0x00); + code.extend_from_slice(&1i32.to_le_bytes()); + code.push(OpCode::Intrinsic as u8); + code.push(0x00); + code.extend_from_slice(&0x1000u32.to_le_bytes()); + code.push(OpCode::Halt as u8); + code.push(0x00); + + let functions = vec![FunctionMeta { + code_offset: 0, + code_len: code.len() as u32, + ..Default::default() + }]; + let res = Verifier::verify(&code, &functions); + assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 6, opcode: OpCode::Intrinsic })); + } + #[test] fn test_verifier_rejects_unpatched_hostcall() { let mut code = Vec::new(); diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 00e39320..4aaaf033 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -1,5 +1,6 @@ use crate::call_frame::CallFrame; use crate::heap::{CoroutineState, Heap}; +use crate::lookup_intrinsic_by_id; use crate::object::ObjectKind; use crate::roots::{RootVisitor, visit_value_for_roots}; use crate::scheduler::Scheduler; @@ -13,8 +14,8 @@ use prometeu_bytecode::decode_next; use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::model::BytecodeModule; use prometeu_bytecode::{ - TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB, - TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, + TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, + TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, }; use prometeu_hal::syscalls::caps::NONE; use prometeu_hal::vm_fault::VmFault; @@ -1391,6 +1392,81 @@ impl VirtualMachine { ))); } } + OpCode::Intrinsic => { + let pc_at_intrinsic = start_pc as u32; + let id = instr + .imm_u32() + .map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; + let intrinsic = lookup_intrinsic_by_id(id).ok_or_else(|| { + self.trap( + TRAP_INVALID_INTRINSIC, + OpCode::Intrinsic as u16, + format!("Unknown intrinsic: 0x{:08X}", id), + pc_at_intrinsic, + ) + })?; + + let args_count = intrinsic.arg_slots(); + let mut args = Vec::with_capacity(args_count); + for _ in 0..args_count { + let value = self.pop().map_err(|_e| { + self.trap( + TRAP_STACK_UNDERFLOW, + OpCode::Intrinsic as u16, + "Intrinsic argument stack underflow".to_string(), + pc_at_intrinsic, + ) + })?; + args.push(value); + } + args.reverse(); + + let results = (intrinsic.implementation)(&args).map_err(|err| match err { + crate::IntrinsicExecutionError::ArityMismatch { expected, got } => self.trap( + TRAP_INVALID_INTRINSIC, + OpCode::Intrinsic as u16, + format!( + "Intrinsic {}.{} argument mismatch: expected {}, got {}", + intrinsic.owner, intrinsic.name, expected, got + ), + pc_at_intrinsic, + ), + crate::IntrinsicExecutionError::TypeMismatch { index, expected } => self.trap( + TRAP_TYPE, + OpCode::Intrinsic as u16, + format!( + "Intrinsic {}.{} argument {} type mismatch: expected {:?}", + intrinsic.owner, intrinsic.name, index, expected + ), + pc_at_intrinsic, + ), + })?; + + intrinsic.validate_result_values(&results).map_err(|err| match err { + crate::IntrinsicExecutionError::ArityMismatch { expected, got } => self.trap( + TRAP_INVALID_INTRINSIC, + OpCode::Intrinsic as u16, + format!( + "Intrinsic {}.{} results mismatch: expected {}, got {}", + intrinsic.owner, intrinsic.name, expected, got + ), + pc_at_intrinsic, + ), + crate::IntrinsicExecutionError::TypeMismatch { index, expected } => self.trap( + TRAP_INVALID_INTRINSIC, + OpCode::Intrinsic as u16, + format!( + "Intrinsic {}.{} result {} type mismatch: expected {:?}", + intrinsic.owner, intrinsic.name, index, expected + ), + pc_at_intrinsic, + ), + })?; + + for value in results { + self.push(value); + } + } OpCode::FrameSync => { // Marks the logical end of a frame: consume cycles and signal to the driver self.cycles += OpCode::FrameSync.cycles(); @@ -2584,6 +2660,82 @@ mod tests { } } + #[test] + fn test_intrinsic_vec2_dot_executes_without_syscalls() { + let mut rom = Vec::new(); + for value in [1.0f64, 2.0, 3.0, 4.0] { + rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes()); + rom.extend_from_slice(&value.to_bits().to_le_bytes()); + } + rom.extend_from_slice(&(OpCode::Intrinsic as u16).to_le_bytes()); + rom.extend_from_slice(&0x1000u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); + assert!(matches!(report.reason, LogicalFrameEndingReason::Halted)); + assert_eq!(vm.operand_stack, vec![Value::Float(11.0)]); + } + + #[test] + fn test_intrinsic_vec2_length_executes_without_syscalls() { + let mut rom = Vec::new(); + for value in [3.0f64, 4.0] { + rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes()); + rom.extend_from_slice(&value.to_bits().to_le_bytes()); + } + rom.extend_from_slice(&(OpCode::Intrinsic as u16).to_le_bytes()); + rom.extend_from_slice(&0x1001u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); + assert!(matches!(report.reason, LogicalFrameEndingReason::Halted)); + assert_eq!(vm.operand_stack, vec![Value::Float(5.0)]); + } + + #[test] + fn test_invalid_intrinsic_trap_is_distinct_from_syscall() { + let rom = assemble("INTRINSIC 0xDEADBEEF\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, TRAP_INVALID_INTRINSIC); + assert_eq!(trap.opcode, OpCode::Intrinsic as u16); + assert!(trap.message.contains("Unknown intrinsic")); + } + _ => panic!("Expected intrinsic trap, got {:?}", report.reason), + } + } + + #[test] + fn test_intrinsic_argument_underflow_trap() { + let rom = assemble("PUSH_I32 1\nINTRINSIC 0x1000\nHALT").expect("assemble"); + let mut vm = new_test_vm(rom, vec![]); + let mut native = MockNative; + let mut ctx = HostContext::new(None); + + let report = vm.run_budget(100, &mut native, &mut ctx).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, TRAP_STACK_UNDERFLOW); + assert_eq!(trap.opcode, OpCode::Intrinsic as u16); + assert!(trap.message.contains("Intrinsic argument stack underflow")); + } + _ => panic!("Expected intrinsic underflow trap, got {:?}", report.reason), + } + } + #[test] fn test_syscall_arg_underflow_trap() { // GfxClear (0x1001) expects 1 arg