dev/pbs #8

Merged
bquarkz merged 74 commits from dev/pbs into master 2026-02-03 15:28:31 +00:00
5 changed files with 361 additions and 104 deletions
Showing only changes of commit 9fa337687f - Show all commits

View File

@ -43,6 +43,8 @@ pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007;
pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008;
/// Attempted to access a local slot that is out of bounds for the current frame.
pub const TRAP_INVALID_LOCAL: u32 = 0x0000_0009;
/// Division or modulo by zero.
pub const TRAP_DIV_ZERO: u32 = 0x0000_000A;
/// Detailed information about a runtime trap.
#[derive(Debug, Clone, PartialEq, Eq)]
@ -84,6 +86,7 @@ mod tests {
assert_eq!(TRAP_INVALID_SYSCALL, 0x07);
assert_eq!(TRAP_STACK_UNDERFLOW, 0x08);
assert_eq!(TRAP_INVALID_LOCAL, 0x09);
assert_eq!(TRAP_DIV_ZERO, 0x0A);
}
#[test]
@ -100,6 +103,7 @@ System Traps:
- INVALID_SYSCALL (0x07): Unknown syscall ID.
- STACK_UNDERFLOW (0x08): Missing syscall arguments.
- INVALID_LOCAL (0x09): Local slot out of bounds.
- DIV_ZERO (0x0A): Division by zero.
Operand Sizes:
- Alloc: 8 bytes (u32 type_id, u32 slots)
@ -110,9 +114,9 @@ Operand Sizes:
// 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\nSystem Traps:\n- INVALID_SYSCALL (0x{:02X}): Unknown syscall ID.\n- STACK_UNDERFLOW (0x{:02X}): Missing syscall arguments.\n- INVALID_LOCAL (0x{:02X}): Local slot out of bounds.\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",
"\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\nSystem Traps:\n- INVALID_SYSCALL (0x{:02X}): Unknown syscall ID.\n- STACK_UNDERFLOW (0x{:02X}): Missing syscall arguments.\n- INVALID_LOCAL (0x{:02X}): Local slot out of bounds.\n- DIV_ZERO (0x{:02X}): Division by zero.\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,
TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW, TRAP_INVALID_LOCAL,
TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW, TRAP_INVALID_LOCAL, TRAP_DIV_ZERO,
operand_size(OpCode::Alloc),
operand_size(OpCode::GateLoad),
operand_size(OpCode::GateStore),

View File

@ -75,6 +75,15 @@ pub enum OpCode {
/// Divides the second top value by the top one (a / b).
/// Stack: [a, b] -> [result]
Div = 0x23,
/// Remainder of the division of the second top value by the top one (a % b).
/// Stack: [a, b] -> [result]
Mod = 0x24,
/// Converts a bounded value to a 64-bit integer.
/// Stack: [bounded] -> [int64]
BoundToInt = 0x25,
/// Converts an integer to a bounded value, trapping if out of range (0..65535).
/// Stack: [int] -> [bounded]
IntToBoundChecked = 0x26,
// --- 6.4 Comparison and Logic ---
@ -234,6 +243,9 @@ impl TryFrom<u16> for OpCode {
0x21 => Ok(OpCode::Sub),
0x22 => Ok(OpCode::Mul),
0x23 => Ok(OpCode::Div),
0x24 => Ok(OpCode::Mod),
0x25 => Ok(OpCode::BoundToInt),
0x26 => Ok(OpCode::IntToBoundChecked),
0x30 => Ok(OpCode::Eq),
0x31 => Ok(OpCode::Neq),
0x32 => Ok(OpCode::Lt),
@ -300,6 +312,9 @@ impl OpCode {
OpCode::Sub => 2,
OpCode::Mul => 4,
OpCode::Div => 6,
OpCode::Mod => 6,
OpCode::BoundToInt => 1,
OpCode::IntToBoundChecked => 1,
OpCode::Eq => 2,
OpCode::Neq => 2,
OpCode::Lt => 2,

View File

@ -40,6 +40,9 @@ impl OpCodeSpecExt for OpCode {
OpCode::Sub => OpcodeSpec { name: "SUB", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Mul => OpcodeSpec { name: "MUL", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Div => OpcodeSpec { name: "DIV", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Mod => OpcodeSpec { name: "MOD", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::BoundToInt => OpcodeSpec { name: "BOUND_TO_INT", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::IntToBoundChecked => OpcodeSpec { name: "INT_TO_BOUND_CHECKED", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Eq => OpcodeSpec { name: "EQ", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Neq => OpcodeSpec { name: "NEQ", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Lt => OpcodeSpec { name: "LT", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },

View File

@ -5,7 +5,7 @@ use crate::virtual_machine::value::Value;
use crate::virtual_machine::{NativeInterface, Program, VmInitError};
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::pbc::{self, ConstantPoolEntry};
use prometeu_bytecode::abi::{TrapInfo, TRAP_OOB};
use prometeu_bytecode::abi::{TrapInfo, TRAP_OOB, TRAP_DIV_ZERO};
/// 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
@ -460,8 +460,21 @@ impl VirtualMachine {
(Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a + *b as f64)),
(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))?,
(Value::Bounded(a), Value::Bounded(b)) => {
let res = a.saturating_add(*b);
if res > 0xFFFF {
Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_OOB,
opcode: OpCode::Add as u16,
message: format!("Bounded addition overflow: {} + {} = {}", a, b, res),
pc: start_pc as u32,
}))
} else {
Ok(Value::Bounded(res))
}
}
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for ADD".into())),
})?,
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))),
@ -472,8 +485,20 @@ impl VirtualMachine {
(Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a - b as f64)),
(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))?,
(Value::Bounded(a), Value::Bounded(b)) => {
if a < b {
Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_OOB,
opcode: OpCode::Sub as u16,
message: format!("Bounded subtraction underflow: {} - {} < 0", a, b),
pc: start_pc as u32,
}))
} else {
Ok(Value::Bounded(a - b))
}
}
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for SUB".into())),
})?,
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))),
@ -484,77 +509,221 @@ impl VirtualMachine {
(Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a * b as f64)),
(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))?,
(Value::Bounded(a), Value::Bounded(b)) => {
let res = a as u64 * b as u64;
if res > 0xFFFF {
Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_OOB,
opcode: OpCode::Mul as u16,
message: format!("Bounded multiplication overflow: {} * {} = {}", a, b, res),
pc: start_pc as u32,
}))
} else {
Ok(Value::Bounded(res as u32))
}
}
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for MUL".into())),
})?,
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()); }
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Integer division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Int32(a / b))
}
(Value::Int64(a), Value::Int64(b)) => {
if b == 0 { return Err("Division by zero".into()); }
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Integer division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Int64(a / b))
}
(Value::Int32(a), Value::Int64(b)) => {
if b == 0 { return Err("Division by zero".into()); }
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Integer division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Int64(a as i64 / b))
}
(Value::Int64(a), Value::Int32(b)) => {
if b == 0 { return Err("Division by zero".into()); }
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Integer division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Int64(a / b as i64))
}
(Value::Float(a), Value::Float(b)) => {
if b == 0.0 { return Err("Division by zero".into()); }
if b == 0.0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Float division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Float(a / b))
}
(Value::Int32(a), Value::Float(b)) => {
if b == 0.0 { return Err("Division by zero".into()); }
if b == 0.0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Float division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Float(a as f64 / b))
}
(Value::Float(a), Value::Int32(b)) => {
if b == 0 { return Err("Division by zero".into()); }
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Float division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Float(a / b as f64))
}
(Value::Int64(a), Value::Float(b)) => {
if b == 0.0 { return Err("Division by zero".into()); }
if b == 0.0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Float division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Float(a as f64 / b))
}
(Value::Float(a), Value::Int64(b)) => {
if b == 0 { return Err("Division by zero".into()); }
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Float division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Float(a / b as f64))
}
_ => Err("Invalid types for DIV".into()),
}).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))?,
(Value::Bounded(a), Value::Bounded(b)) => {
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Div as u16,
message: "Bounded division by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Bounded(a / b))
}
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for DIV".into())),
})?,
OpCode::Mod => self.binary_op(|a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => {
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Mod as u16,
message: "Integer modulo by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Int32(a % b))
}
(Value::Int64(a), Value::Int64(b)) => {
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Mod as u16,
message: "Integer modulo by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Int64(a % b))
}
(Value::Bounded(a), Value::Bounded(b)) => {
if b == 0 {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_DIV_ZERO,
opcode: OpCode::Mod as u16,
message: "Bounded modulo by zero".into(),
pc: start_pc as u32,
}));
}
Ok(Value::Bounded(a % b))
}
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for MOD".into())),
})?,
OpCode::BoundToInt => {
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Bounded(b) = val {
self.push(Value::Int64(b as i64));
} else {
return Err(LogicalFrameEndingReason::Panic("Expected bounded for BOUND_TO_INT".into()));
}
}
OpCode::IntToBoundChecked => {
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let int_val = val.as_integer().ok_or_else(|| LogicalFrameEndingReason::Panic("Expected integer for INT_TO_BOUND_CHECKED".into()))?;
if int_val < 0 || int_val > 0xFFFF {
return Err(LogicalFrameEndingReason::Trap(TrapInfo {
code: TRAP_OOB,
opcode: OpCode::IntToBoundChecked as u16,
message: format!("Integer to bounded conversion out of range: {}", int_val),
pc: start_pc as u32,
}));
}
self.push(Value::Bounded(int_val as u32));
}
OpCode::Eq => self.binary_op(|a, b| Ok(Value::Boolean(a == b)))?,
OpCode::Neq => self.binary_op(|a, b| Ok(Value::Boolean(a != b)))?,
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))?,
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for LT".into()))
})?,
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))?,
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for GT".into()))
})?,
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))?,
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for LTE".into()))
})?,
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))?,
.ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid types for GTE".into()))
})?,
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))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for AND".into())),
})?,
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))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for OR".into())),
})?,
OpCode::Not => {
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Boolean(b) = val {
@ -568,36 +737,36 @@ impl VirtualMachine {
(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 BitAnd".into()),
}).map_err(|e| LogicalFrameEndingReason::Panic(e))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for BitAnd".into())),
})?,
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))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for BitOr".into())),
})?,
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))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for BitXor".into())),
})?,
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))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for Shl".into())),
})?,
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))?,
_ => Err(LogicalFrameEndingReason::Panic("Invalid types for Shr".into())),
})?,
OpCode::Neg => {
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
match val {
@ -829,12 +998,12 @@ impl VirtualMachine {
self.operand_stack.last().ok_or("Stack underflow".into())
}
fn binary_op<F>(&mut self, f: F) -> Result<(), String>
fn binary_op<F>(&mut self, f: F) -> Result<(), LogicalFrameEndingReason>
where
F: FnOnce(Value, Value) -> Result<Value, String>,
F: FnOnce(Value, Value) -> Result<Value, LogicalFrameEndingReason>,
{
let b = self.pop()?;
let a = self.pop()?;
let b = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let a = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let res = f(a, b)?;
self.push(res);
Ok(())
@ -869,6 +1038,128 @@ mod tests {
fn assets_mut(&mut self) -> &mut crate::hardware::AssetManager { todo!() }
}
#[test]
fn test_arithmetic_chain() {
let mut native = MockNative;
let mut hw = MockHardware;
// (10 + 20) * 2 / 5 % 4 = 12 * 2 / 5 % 4 = 60 / 5 % 4 = 12 % 4 = 0
// wait: (10 + 20) = 30. 30 * 2 = 60. 60 / 5 = 12. 12 % 4 = 0.
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&10i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&20i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&2i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Mul as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&5i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&4i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Mod as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = VirtualMachine::new(rom, vec![]);
vm.run_budget(100, &mut native, &mut hw).unwrap();
assert_eq!(vm.pop().unwrap(), Value::Int32(0));
}
#[test]
fn test_div_by_zero_trap() {
let mut native = MockNative;
let mut hw = MockHardware;
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&10i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&0i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Div as u16).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, TRAP_DIV_ZERO);
assert_eq!(trap.opcode, OpCode::Div as u16);
}
_ => panic!("Expected Trap, got {:?}", report.reason),
}
}
#[test]
fn test_int_to_bound_checked_trap() {
let mut native = MockNative;
let mut hw = MockHardware;
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&70000i32.to_le_bytes()); // > 65535
rom.extend_from_slice(&(OpCode::IntToBoundChecked as u16).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, TRAP_OOB);
assert_eq!(trap.opcode, OpCode::IntToBoundChecked as u16);
}
_ => panic!("Expected Trap, got {:?}", report.reason),
}
}
#[test]
fn test_bounded_add_overflow_trap() {
let mut native = MockNative;
let mut hw = MockHardware;
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushBounded as u16).to_le_bytes());
rom.extend_from_slice(&60000u32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushBounded as u16).to_le_bytes());
rom.extend_from_slice(&10000u32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Add as u16).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, TRAP_OOB);
assert_eq!(trap.opcode, OpCode::Add as u16);
}
_ => panic!("Expected Trap, got {:?}", report.reason),
}
}
#[test]
fn test_comparisons_polymorphic() {
let mut native = MockNative;
let mut hw = MockHardware;
// 10 < 20.5 (true)
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&10i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes());
rom.extend_from_slice(&20.5f64.to_le_bytes());
rom.extend_from_slice(&(OpCode::Lt as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = VirtualMachine::new(rom, vec![]);
vm.run_budget(100, &mut native, &mut hw).unwrap();
assert_eq!(vm.pop().unwrap(), Value::Boolean(true));
}
#[test]
fn test_push_i64_immediate() {
let mut rom = Vec::new();

View File

@ -1,37 +1,3 @@
## PR-05 — Core arithmetic + comparisons in VM (int/bounded/bool)
**Why:** The minimal executable PBS needs arithmetic that doesnt corrupt stack.
### Scope
* Implement v0 numeric opcodes (slot-safe):
* `IADD, ISUB, IMUL, IDIV, IMOD`
* `ICMP_EQ, ICMP_NE, ICMP_LT, ICMP_LE, ICMP_GT, ICMP_GE`
* `BADD, BSUB, ...` (or unify with tagged values)
* Define conversion opcodes if lowering expects them:
* `BOUND_TO_INT`, `INT_TO_BOUND_CHECKED` (trap OOB)
### Deliverables
* Deterministic traps:
* `TRAP_DIV_ZERO`
* `TRAP_OOB` (bounded checks)
### Tests
* simple arithmetic chain
* div by zero traps
* bounded conversions trap on overflow
### Acceptance
* Arithmetic and comparisons are closed and verified.
---
## PR-06 — Control flow opcodes: jumps, conditional branches, structured “if”
**Why:** `if` must be predictable and verifier-safe.
@ -263,28 +229,6 @@
---
# Work split (what can be parallel later)
* VM core correctness: PR-01..PR-08 (sequential, contract-first)
* Debug + tooling: PR-09, PR-12 (parallel after PR-03)
* Linking/imports: PR-10 (parallel after PR-01)
* Canonical cartridge: PR-11 (parallel after PR-05)
---
# “Stop the line” rules
1. If a PR introduces an opcode without stack spec + verifier integration, its rejected.
2. If a PR changes bytecode layout without bumping version, its rejected.
3. If a PR adds a feature before the canonical cartridge passes, its rejected.
---
# First implementation target (tomorrow morning, start here)
**Start with PR-02 (Opcode spec + verifier)** even if you think you already know the bug.
Once the verifier exists, the rest becomes mechanical: every failure becomes *actionable*.
## Definition of Done (DoD) for PBS v0 “minimum executable”
A single canonical cartridge runs end-to-end: