This commit is contained in:
Nilton Constantino 2026-01-30 16:21:02 +00:00
parent c797be9287
commit 8c161e3e13
No known key found for this signature in database
11 changed files with 261 additions and 140 deletions

View File

@ -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());
}
}

View File

@ -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<ConstantPoolEntry>,
/// 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<PbcFile, String> {
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<PbcFile, String> {
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<Vec<u8>, 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,
};

View File

@ -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,
};

View File

@ -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.

View File

@ -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(),
}
}

View File

@ -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),
}
}
}

View File

@ -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