pr 35
This commit is contained in:
parent
c797be9287
commit
8c161e3e13
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user