dev/pbs #8
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,37 +1,3 @@
|
||||
## PR-05 — Core arithmetic + comparisons in VM (int/bounded/bool)
|
||||
|
||||
**Why:** The minimal executable PBS needs arithmetic that doesn’t 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, it’s rejected.
|
||||
2. If a PR changes bytecode layout without bumping version, it’s rejected.
|
||||
3. If a PR adds a feature before the canonical cartridge passes, it’s 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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user