diff --git a/crates/prometeu-bytecode/src/abi.rs b/crates/prometeu-bytecode/src/abi.rs index 777bcf53..1dcf0157 100644 --- a/crates/prometeu-bytecode/src/abi.rs +++ b/crates/prometeu-bytecode/src/abi.rs @@ -20,10 +20,36 @@ pub fn operand_size(opcode: OpCode) -> usize { OpCode::GetLocal | OpCode::SetLocal => 4, OpCode::Call => 8, // addr(u32) + args_count(u32) OpCode::Syscall => 4, + OpCode::Alloc => 8, // type_id(u32) + slots(u32) + OpCode::GateLoad | OpCode::GateStore => 4, // offset(u32) _ => 0, } } +// --- HIP Trap Codes --- + +/// Attempted to access a gate that does not exist or has been recycled incorrectly. +pub const TRAP_INVALID_GATE: u32 = 0x01; +/// Attempted to access a gate that has been explicitly released (RC=0). +pub const TRAP_DEAD_GATE: u32 = 0x02; +/// Attempted to access a field or index beyond the allocated slots for a gate. +pub const TRAP_OOB: u32 = 0x03; +/// Attempted a typed operation on a gate whose storage type does not match. +pub const TRAP_TYPE: u32 = 0x04; + +/// Detailed information about a runtime trap. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TrapInfo { + /// The specific trap code (e.g., TRAP_OOB). + pub code: u32, + /// The numeric value of the opcode that triggered the trap. + pub opcode: u16, + /// A human-readable message explaining the trap. + pub message: String, + /// The absolute Program Counter (PC) address where the trap occurred. + pub pc: u32, +} + /// Checks if an instruction is a jump (branch) instruction. pub fn is_jump(opcode: OpCode) -> bool { match opcode { @@ -36,3 +62,47 @@ pub fn is_jump(opcode: OpCode) -> bool { pub fn has_immediate(opcode: OpCode) -> bool { operand_size(opcode) > 0 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trap_code_stability() { + // These numeric values are normative and must not change. + assert_eq!(TRAP_INVALID_GATE, 0x01); + assert_eq!(TRAP_DEAD_GATE, 0x02); + assert_eq!(TRAP_OOB, 0x03); + assert_eq!(TRAP_TYPE, 0x04); + } + + #[test] + fn test_abi_documentation_snapshot() { + // Snapshot of the ABI rules for traps and operands. + let abi_info = r#" +HIP Traps: +- INVALID_GATE (0x01): Non-existent gate handle. +- DEAD_GATE (0x02): Gate handle with RC=0. +- OOB (0x03): Access beyond allocated slots. +- TYPE (0x04): Type mismatch during heap access. + +Operand Sizes: +- Alloc: 8 bytes (u32 type_id, u32 slots) +- GateLoad: 4 bytes (u32 offset) +- GateStore: 4 bytes (u32 offset) +- PopN: 4 bytes (u32 count) +"#; + // This test serves as a "doc-lock". + // If you change the ABI, you must update this string. + let current_info = format!( + "\nHIP Traps:\n- INVALID_GATE (0x{:02X}): Non-existent gate handle.\n- DEAD_GATE (0x{:02X}): Gate handle with RC=0.\n- OOB (0x{:02X}): Access beyond allocated slots.\n- TYPE (0x{:02X}): Type mismatch during heap access.\n\nOperand Sizes:\n- Alloc: {} bytes (u32 type_id, u32 slots)\n- GateLoad: {} bytes (u32 offset)\n- GateStore: {} bytes (u32 offset)\n- PopN: {} bytes (u32 count)\n", + TRAP_INVALID_GATE, TRAP_DEAD_GATE, TRAP_OOB, TRAP_TYPE, + operand_size(OpCode::Alloc), + operand_size(OpCode::GateLoad), + operand_size(OpCode::GateStore), + operand_size(OpCode::PopN) + ); + + assert_eq!(current_info.trim(), abi_info.trim()); + } +} diff --git a/crates/prometeu-bytecode/src/pbc.rs b/crates/prometeu-bytecode/src/pbc.rs index beee674e..39b327d3 100644 --- a/crates/prometeu-bytecode/src/pbc.rs +++ b/crates/prometeu-bytecode/src/pbc.rs @@ -26,12 +26,16 @@ pub enum ConstantPoolEntry { /// /// The file format follows this structure (Little-Endian): /// 1. Magic Header: "PPBC" (4 bytes) -/// 2. CP Count: u32 -/// 3. CP Entries: [Tag (u8), Data...] -/// 4. ROM Size: u32 -/// 5. ROM Data: [u16 OpCode, Operands...][] +/// 2. Version: u16 (Currently 0) +/// 3. Flags: u16 (Reserved) +/// 4. CP Count: u32 +/// 5. CP Entries: [Tag (u8), Data...] +/// 6. ROM Size: u32 +/// 7. ROM Data: [u16 OpCode, Operands...][] #[derive(Debug, Clone, Default)] pub struct PbcFile { + /// The file format version. + pub version: u16, /// The list of constants used by the program. pub cp: Vec, /// The raw instruction bytes (ROM). @@ -43,12 +47,15 @@ pub struct PbcFile { /// This function validates the "PPBC" signature and reconstructs the /// Constant Pool and ROM data from the binary format. pub fn parse_pbc(bytes: &[u8]) -> Result { - if bytes.len() < 4 || &bytes[0..4] != b"PPBC" { + if bytes.len() < 8 || &bytes[0..4] != b"PPBC" { return Err("Invalid PBC signature".into()); } let mut cursor = Cursor::new(&bytes[4..]); + let version = read_u16_le(&mut cursor).map_err(|e| e.to_string())?; + let _flags = read_u16_le(&mut cursor).map_err(|e| e.to_string())?; + let cp_count = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize; let mut cp = Vec::with_capacity(cp_count); @@ -90,7 +97,7 @@ pub fn parse_pbc(bytes: &[u8]) -> Result { let mut rom = vec![0u8; rom_size]; cursor.read_exact(&mut rom).map_err(|e| e.to_string())?; - Ok(PbcFile { cp, rom }) + Ok(PbcFile { version, cp, rom }) } /// Serializes a `PbcFile` structure into a binary buffer. @@ -100,6 +107,9 @@ pub fn write_pbc(pbc: &PbcFile) -> Result, String> { let mut out = Vec::new(); out.write_all(b"PPBC").map_err(|e| e.to_string())?; + write_u16_le(&mut out, pbc.version).map_err(|e| e.to_string())?; + write_u16_le(&mut out, 0).map_err(|e| e.to_string())?; // Flags reserved + write_u32_le(&mut out, pbc.cp.len() as u32).map_err(|e| e.to_string())?; for entry in &pbc.cp { @@ -157,6 +167,7 @@ mod tests { // 2. Create a PBC file let pbc_file = PbcFile { + version: 0, cp: vec![ConstantPoolEntry::Int32(100)], // Random CP entry rom, }; diff --git a/crates/prometeu-compiler/src/backend/emit_bytecode.rs b/crates/prometeu-compiler/src/backend/emit_bytecode.rs index be04eb5d..ee0590f9 100644 --- a/crates/prometeu-compiler/src/backend/emit_bytecode.rs +++ b/crates/prometeu-compiler/src/backend/emit_bytecode.rs @@ -227,6 +227,7 @@ impl<'a> BytecodeEmitter<'a> { // --- PHASE 4: Serialization --- // Packages the constant pool and bytecode into the final PBC format. let pbc = PbcFile { + version: 0, cp: self.constant_pool.clone(), rom: bytecode, }; diff --git a/crates/prometeu-core/src/hardware/gfx.rs b/crates/prometeu-core/src/hardware/gfx.rs index cd0306dc..2a429de2 100644 --- a/crates/prometeu-core/src/hardware/gfx.rs +++ b/crates/prometeu-core/src/hardware/gfx.rs @@ -6,10 +6,8 @@ use std::sync::Arc; /// Defines how source pixels are combined with existing pixels in the framebuffer. /// /// ### Usage Example: -/// ```rust /// // Draw a semi-transparent blue rectangle /// gfx.fill_rect_blend(10, 10, 50, 50, Color::BLUE, BlendMode::Half); -/// ``` #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum BlendMode { /// No blending: a source overwrites the destination. diff --git a/crates/prometeu-core/src/virtual_machine/value.rs b/crates/prometeu-core/src/virtual_machine/value.rs index 5a3c8aec..af423345 100644 --- a/crates/prometeu-core/src/virtual_machine/value.rs +++ b/crates/prometeu-core/src/virtual_machine/value.rs @@ -21,7 +21,7 @@ pub enum Value { /// UTF-8 string. Strings are immutable and usually come from the Constant Pool. String(String), /// A pointer to an object on the heap. - Ref(usize), + Gate(usize), /// Represents the absence of a value (equivalent to `null` or `undefined`). Null, } @@ -40,7 +40,7 @@ impl PartialEq for Value { (Value::Float(a), Value::Int64(b)) => *a == *b as f64, (Value::Boolean(a), Value::Boolean(b)) => a == b, (Value::String(a), Value::String(b)) => a == b, - (Value::Ref(a), Value::Ref(b)) => a == b, + (Value::Gate(a), Value::Gate(b)) => a == b, (Value::Null, Value::Null) => true, _ => false, } @@ -92,7 +92,7 @@ impl Value { Value::Float(f) => f.to_string(), Value::Boolean(b) => b.to_string(), Value::String(s) => s.clone(), - Value::Ref(r) => format!("[Ref {}]", r), + Value::Gate(r) => format!("[Gate {}]", r), Value::Null => "null".to_string(), } } diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index ebd793e0..ed5bf582 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -5,11 +5,12 @@ use crate::virtual_machine::value::Value; use crate::virtual_machine::{NativeInterface, Program}; use prometeu_bytecode::opcode::OpCode; use prometeu_bytecode::pbc::{self, ConstantPoolEntry}; +use prometeu_bytecode::abi::TrapInfo; /// Reason why the Virtual Machine stopped execution during a specific run. /// This allows the system to decide if it should continue execution in the next tick /// or if the frame is finalized. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum LogicalFrameEndingReason { /// Execution reached a `FRAME_SYNC` instruction, marking the end of the logical frame. FrameSync, @@ -21,10 +22,14 @@ pub enum LogicalFrameEndingReason { EndOfRom, /// Execution hit a registered breakpoint. Breakpoint, + /// A runtime trap occurred (e.g., OOB, invalid gate). + Trap(TrapInfo), + /// A fatal error occurred that cannot be recovered (e.g., stack underflow). + Panic(String), } /// A report detailing the results of an execution slice (run_budget). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BudgetReport { /// Total virtual cycles consumed during this run. pub cycles_used: u64, @@ -198,13 +203,17 @@ impl VirtualMachine { } // Execute a single step (Fetch-Decode-Execute) - self.step(native, hw)?; + if let Err(reason) = self.step(native, hw) { + ending_reason = Some(reason); + break; + } steps_executed += 1; // Integrity check: ensure real progress is being made to avoid infinite loops // caused by zero-cycle instructions or stuck PC. if self.pc == pc_before && self.cycles == cycles_before && !self.halted { - return Err(format!("VM stuck at PC 0x{:08X}", self.pc)); + ending_reason = Some(LogicalFrameEndingReason::Panic(format!("VM stuck at PC 0x{:08X}", self.pc))); + break; } } @@ -244,14 +253,16 @@ impl VirtualMachine { /// 1. Fetch: Read the opcode from memory. /// 2. Decode: Identify what operation to perform. /// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals. - pub fn step(&mut self, native: &mut dyn NativeInterface, hw: &mut dyn HardwareBridge) -> Result<(), String> { + pub fn step(&mut self, native: &mut dyn NativeInterface, hw: &mut dyn HardwareBridge) -> Result<(), LogicalFrameEndingReason> { if self.halted || self.pc >= self.program.rom.len() { return Ok(()); } + let start_pc = self.pc; + // Fetch & Decode - let opcode_val = self.read_u16()?; - let opcode = OpCode::try_from(opcode_val)?; + let opcode_val = self.read_u16().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let opcode = OpCode::try_from(opcode_val).map_err(|e| LogicalFrameEndingReason::Panic(e))?; // Execute match opcode { @@ -260,19 +271,19 @@ impl VirtualMachine { self.halted = true; } OpCode::Jmp => { - let addr = self.read_u32()? as usize; + let addr = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; self.pc = addr; } OpCode::JmpIfFalse => { - let addr = self.read_u32()? as usize; - let val = self.pop()?; + let addr = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; if let Value::Boolean(false) = val { self.pc = addr; } } OpCode::JmpIfTrue => { - let addr = self.read_u32()? as usize; - let val = self.pop()?; + let addr = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; if let Value::Boolean(true) = val { self.pc = addr; } @@ -282,42 +293,42 @@ impl VirtualMachine { // but we need to advance PC if executed via step() directly. } OpCode::PushConst => { - let idx = self.read_u32()? as usize; - let val = self.program.constant_pool.get(idx).cloned().ok_or("Invalid constant index")?; + let idx = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid constant index".into()))?; self.push(val); } OpCode::PushI64 => { - let val = self.read_i64()?; + let val = self.read_i64().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.push(Value::Int64(val)); } OpCode::PushI32 => { - let val = self.read_i32()?; + let val = self.read_i32().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.push(Value::Int32(val)); } OpCode::PushF64 => { - let val = self.read_f64()?; + let val = self.read_f64().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.push(Value::Float(val)); } OpCode::PushBool => { - let val = self.read_u8()?; + let val = self.read_u8().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.push(Value::Boolean(val != 0)); } OpCode::Pop => { - self.pop()?; + self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; } OpCode::PopN => { - let n = self.read_u16()?; + let n = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))?; for _ in 0..n { - self.pop()?; + self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; } } OpCode::Dup => { - let val = self.peek()?.clone(); + let val = self.peek().map_err(|e| LogicalFrameEndingReason::Panic(e))?.clone(); self.push(val); } OpCode::Swap => { - let a = self.pop()?; - let b = self.pop()?; + let a = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let b = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.push(a); self.push(b); } @@ -335,7 +346,7 @@ impl VirtualMachine { (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a + *b as f64)), _ => Err("Invalid types for ADD".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Sub => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_sub(b))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_sub(b))), @@ -347,7 +358,7 @@ impl VirtualMachine { (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)), _ => Err("Invalid types for SUB".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Mul => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_mul(b))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_mul(b))), @@ -359,7 +370,7 @@ impl VirtualMachine { (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a * b as f64)), _ => Err("Invalid types for MUL".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Div => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => { if b == 0 { return Err("Division by zero".into()); } @@ -398,43 +409,43 @@ impl VirtualMachine { Ok(Value::Float(a / b as f64)) } _ => Err("Invalid types for DIV".into()), - })?, - OpCode::Eq => self.binary_op(|a, b| Ok(Value::Boolean(a == b)))?, - OpCode::Neq => self.binary_op(|a, b| Ok(Value::Boolean(a != b)))?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, + OpCode::Eq => self.binary_op(|a, b| Ok(Value::Boolean(a == b))).map_err(|e| LogicalFrameEndingReason::Panic(e))?, + OpCode::Neq => self.binary_op(|a, b| Ok(Value::Boolean(a != b))).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Lt => self.binary_op(|a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o == std::cmp::Ordering::Less)) .ok_or_else(|| "Invalid types for LT".into()) - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Gt => self.binary_op(|a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o == std::cmp::Ordering::Greater)) .ok_or_else(|| "Invalid types for GT".into()) - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Lte => self.binary_op(|a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o != std::cmp::Ordering::Greater)) .ok_or_else(|| "Invalid types for LTE".into()) - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Gte => self.binary_op(|a, b| { a.partial_cmp(&b) .map(|o| Value::Boolean(o != std::cmp::Ordering::Less)) .ok_or_else(|| "Invalid types for GTE".into()) - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::And => self.binary_op(|a, b| match (a, b) { (Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a && b)), _ => Err("Invalid types for AND".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Or => self.binary_op(|a, b| match (a, b) { (Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a || b)), _ => Err("Invalid types for OR".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Not => { - let val = self.pop()?; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; if let Value::Boolean(b) = val { self.push(Value::Boolean(!b)); } else { - return Err("Invalid type for NOT".into()); + return Err(LogicalFrameEndingReason::Panic("Invalid type for NOT".into())); } } OpCode::BitAnd => self.binary_op(|a, b| match (a, b) { @@ -443,78 +454,76 @@ impl VirtualMachine { (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) & b)), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a & (b as i64))), _ => Err("Invalid types for BitAnd".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::BitOr => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a | b)), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a | b)), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) | b)), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a | (b as i64))), _ => Err("Invalid types for BitOr".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::BitXor => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a ^ b)), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a ^ b)), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) ^ b)), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a ^ (b as i64))), _ => Err("Invalid types for BitXor".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Shl => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), _ => Err("Invalid types for Shl".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Shr => self.binary_op(|a, b| match (a, b) { (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), _ => Err("Invalid types for Shr".into()), - })?, + }).map_err(|e| LogicalFrameEndingReason::Panic(e))?, OpCode::Neg => { - let val = self.pop()?; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; match val { Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())), Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())), Value::Float(a) => self.push(Value::Float(-a)), - _ => return Err("Invalid type for Neg".into()), + _ => return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into())), } } OpCode::GetGlobal => { - let idx = self.read_u32()? as usize; - let val = self.globals.get(idx).cloned().ok_or("Invalid global index")?; + let idx = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.globals.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid global index".into()))?; self.push(val); } OpCode::SetGlobal => { - let idx = self.read_u32()? as usize; - let val = self.pop()?; + let idx = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; if idx >= self.globals.len() { self.globals.resize(idx + 1, Value::Null); } self.globals[idx] = val; } OpCode::GetLocal => { - let idx = self.read_u32()? as usize; - let frame = self.call_stack.last().ok_or("No active call frame")?; - let val = self.operand_stack.get(frame.stack_base + idx).cloned().ok_or("Invalid local index")?; + let idx = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?; + let val = self.operand_stack.get(frame.stack_base + idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid local index".into()))?; self.push(val); } OpCode::SetLocal => { - let idx = self.read_u32()? as usize; - let val = self.pop()?; - let frame = self.call_stack.last().ok_or("No active call frame")?; + let idx = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?; let stack_idx = frame.stack_base + idx; if stack_idx >= self.operand_stack.len() { - return Err("Local index out of bounds".into()); + return Err(LogicalFrameEndingReason::Panic("Local index out of bounds".into())); } self.operand_stack[stack_idx] = val; } OpCode::Call => { - // addr: destination instruction address - // args_count: how many values from the operand stack become locals in the new frame - let addr = self.read_u32()? as usize; - let args_count = self.read_u32()? as usize; + let addr = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let args_count = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; let stack_base = self.operand_stack.len() - args_count; self.call_stack.push(CallFrame { return_pc: self.pc as u32, @@ -523,81 +532,89 @@ impl VirtualMachine { self.pc = addr; } OpCode::Ret => { - let frame = self.call_stack.pop().ok_or("Call stack underflow")?; - // ABI Rule: Every function MUST leave exactly one value on the stack before RET. - // This value is popped before cleaning the stack and re-pushed after. - let return_val = self.pop()?; - // Clean up the operand stack, removing the frame's locals + let frame = self.call_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Call stack underflow".into()))?; + let return_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.operand_stack.truncate(frame.stack_base); - // Return the result of the function self.push(return_val); self.pc = frame.return_pc as usize; } OpCode::PushScope => { - // Used for blocks within a function that have their own locals self.scope_stack.push(ScopeFrame { scope_stack_base: self.operand_stack.len(), }); } OpCode::PopScope => { - let frame = self.scope_stack.pop().ok_or("Scope stack underflow")?; + let frame = self.scope_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Scope stack underflow".into()))?; self.operand_stack.truncate(frame.scope_stack_base); } OpCode::Alloc => { - // Allocates 'slots' values on the heap and pushes a reference to the stack - let _type_id = self.read_u32()?; - let slots = self.read_u32()? as usize; + let _type_id = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let slots = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; let ref_idx = self.heap.len(); for _ in 0..slots { self.heap.push(Value::Null); } - self.push(Value::Ref(ref_idx)); + self.push(Value::Gate(ref_idx)); } OpCode::GateLoad => { - // Reads a value from a heap reference at a specific offset - let offset = self.read_u32()? as usize; - let ref_val = self.pop()?; - if let Value::Ref(base) = ref_val { - let val = self.heap.get(base + offset).cloned().ok_or("Invalid heap access")?; + let offset = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + if let Value::Gate(base) = ref_val { + let val = self.heap.get(base + offset).cloned().ok_or_else(|| { + LogicalFrameEndingReason::Trap(TrapInfo { + code: prometeu_bytecode::abi::TRAP_OOB, + opcode: OpCode::GateLoad as u16, + message: format!("Out-of-bounds heap access at offset {}", offset), + pc: start_pc as u32, + }) + })?; self.push(val); } else { - return Err("Expected reference for GATE_LOAD".into()); + return Err(LogicalFrameEndingReason::Trap(TrapInfo { + code: prometeu_bytecode::abi::TRAP_TYPE, + opcode: OpCode::GateLoad as u16, + message: "Expected gate handle for GATE_LOAD".to_string(), + pc: start_pc as u32, + })); } } OpCode::GateStore => { - // Writes a value to a heap reference at a specific offset - let offset = self.read_u32()? as usize; - let val = self.pop()?; - let ref_val = self.pop()?; - if let Value::Ref(base) = ref_val { + let offset = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))? as usize; + let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + if let Value::Gate(base) = ref_val { if base + offset >= self.heap.len() { - return Err("Invalid heap access".into()); + return Err(LogicalFrameEndingReason::Trap(TrapInfo { + code: prometeu_bytecode::abi::TRAP_OOB, + opcode: OpCode::GateStore as u16, + message: format!("Out-of-bounds heap access at offset {}", offset), + pc: start_pc as u32, + })); } self.heap[base + offset] = val; } else { - return Err("Expected reference for GATE_STORE".into()); + return Err(LogicalFrameEndingReason::Trap(TrapInfo { + code: prometeu_bytecode::abi::TRAP_TYPE, + opcode: OpCode::GateStore as u16, + message: "Expected gate handle for GATE_STORE".to_string(), + pc: start_pc as u32, + })); } } OpCode::GateBeginPeek | OpCode::GateEndPeek | OpCode::GateBeginBorrow | OpCode::GateEndBorrow | OpCode::GateBeginMutate | OpCode::GateEndMutate | OpCode::GateRetain => { - // These are no-ops in v0, but they preserve the gate on the stack. } OpCode::GateRelease => { - self.pop()?; + self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; } OpCode::Syscall => { - // Calls a native function implemented by the Firmware/OS. - // ABI Rule: Arguments are pushed in call order (LIFO). - // The native implementation is responsible for popping all arguments - // and pushing a return value if applicable. - let id = self.read_u32()?; - let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?; + let id = self.read_u32().map_err(|e| LogicalFrameEndingReason::Panic(e))?; + let native_cycles = native.syscall(id, self, hw).map_err(|e| LogicalFrameEndingReason::Panic(format!("syscall 0x{:08X} failed: {}", id, e)))?; self.cycles += native_cycles; } OpCode::FrameSync => { - // Already handled in the run_budget loop for performance return Ok(()); } } @@ -887,7 +904,10 @@ mod tests { vm.step(&mut native, &mut hw).unwrap(); // CALL let res = vm.step(&mut native, &mut hw); // RET -> should fail assert!(res.is_err()); - assert!(res.unwrap_err().contains("Stack underflow")); + match res.unwrap_err() { + LogicalFrameEndingReason::Panic(msg) => assert!(msg.contains("Stack underflow")), + _ => panic!("Expected Panic"), + } // Agora com valor de retorno let mut rom2 = Vec::new(); @@ -1194,7 +1214,7 @@ mod tests { rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); rom.extend_from_slice(&3i32.to_le_bytes()); rom.extend_from_slice(&(OpCode::PopN as u16).to_le_bytes()); - rom.extend_from_slice(&2u16.to_le_bytes()); + rom.extend_from_slice(&2u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); let mut vm = VirtualMachine::new(rom, vec![]); @@ -1203,4 +1223,58 @@ mod tests { assert_eq!(vm.pop().unwrap(), Value::Int32(1)); assert!(vm.pop().is_err()); // Stack should be empty } + + #[test] + fn test_hip_traps_oob() { + let mut native = MockNative; + let mut hw = MockHardware; + + // ALLOC int, 1 -> Gate(0) + // GATE_LOAD 1 -> TRAP_OOB (size is 1, offset 1 is invalid) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::Alloc as u16).to_le_bytes()); + rom.extend_from_slice(&0u32.to_le_bytes()); // type_id + rom.extend_from_slice(&1u32.to_le_bytes()); // slots + rom.extend_from_slice(&(OpCode::GateLoad as u16).to_le_bytes()); + rom.extend_from_slice(&1u32.to_le_bytes()); // offset 1 + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_OOB); + assert_eq!(trap.opcode, OpCode::GateLoad as u16); + assert!(trap.message.contains("Out-of-bounds")); + } + _ => panic!("Expected Trap, got {:?}", report.reason), + } + } + + #[test] + fn test_hip_traps_type() { + let mut native = MockNative; + let mut hw = MockHardware; + + // PUSH_I32 42 + // GATE_LOAD 0 -> TRAP_TYPE (Expected gate handle, got Int32) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::GateLoad as u16).to_le_bytes()); + rom.extend_from_slice(&0u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_TYPE); + assert_eq!(trap.opcode, OpCode::GateLoad as u16); + } + _ => panic!("Expected Trap, got {:?}", report.reason), + } + } } diff --git a/docs/specs/pbs/specs/PBS - Canonical Addenda.md b/docs/specs/pbs/PBS - Canonical Addenda.md similarity index 100% rename from docs/specs/pbs/specs/PBS - Canonical Addenda.md rename to docs/specs/pbs/PBS - Canonical Addenda.md diff --git a/docs/specs/pbs/specs/Prometeu Base Script (PBS) - Implementation Spec.md b/docs/specs/pbs/Prometeu Base Script (PBS) - Implementation Spec.md similarity index 100% rename from docs/specs/pbs/specs/Prometeu Base Script (PBS) - Implementation Spec.md rename to docs/specs/pbs/Prometeu Base Script (PBS) - Implementation Spec.md diff --git a/docs/specs/pbs/specs/Prometeu Scripting - Prometeu Bytecode Script (PBS).md b/docs/specs/pbs/Prometeu Scripting - Prometeu Bytecode Script (PBS).md similarity index 100% rename from docs/specs/pbs/specs/Prometeu Scripting - Prometeu Bytecode Script (PBS).md rename to docs/specs/pbs/Prometeu Scripting - Prometeu Bytecode Script (PBS).md diff --git a/docs/specs/pbs/specs/Prometeu VM Memory model.md b/docs/specs/pbs/Prometeu VM Memory model.md similarity index 100% rename from docs/specs/pbs/specs/Prometeu VM Memory model.md rename to docs/specs/pbs/Prometeu VM Memory model.md diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index a5364065..e86752ee 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -1,36 +1,3 @@ -## PR-10 — HIP ABI Freeze v0: Trap Conditions + Debug Surface - -### Goal - -Freeze the runtime-visible ABI behavior for HIP operations. - -### Required Content (Normative) - -Add a document (or module-level docs) defining traps: - -* Invalid `GateId` → trap `TRAP_INVALID_GATE` -* Dead gate access → trap `TRAP_DEAD_GATE` -* Out-of-bounds offset (`offset >= slots`) → trap `TRAP_OOB` -* Type mismatch (if enforced) → trap `TRAP_TYPE` - -Define what a trap includes: - -* opcode -* message -* optional span (if debug info is present) - -### Required Changes - -* Add trap codes/constants in bytecode/VM interface. -* Ensure bytecode format reserves space / structure for propagating trap info. - -### Tests (Mandatory) - -* Unit tests verifying trap codes are stable (numeric values frozen). -* Doc tests or snapshot for ABI text. - ---- - ## PR-11 — Cross-Layer Conformance Tests: Core→VM→Bytecode (HIP) ### Goal