From 9fa337687fb1ed573e026d4febcaf7081a293e00 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Sat, 31 Jan 2026 17:45:31 +0000 Subject: [PATCH] pr 45 --- crates/prometeu-bytecode/src/abi.rs | 8 +- crates/prometeu-bytecode/src/opcode.rs | 15 + .../src/virtual_machine/opcode_spec.rs | 3 + .../src/virtual_machine/virtual_machine.rs | 383 +++++++++++++++--- docs/specs/pbs/files/PRs para Junie.md | 56 --- 5 files changed, 361 insertions(+), 104 deletions(-) diff --git a/crates/prometeu-bytecode/src/abi.rs b/crates/prometeu-bytecode/src/abi.rs index f5bbd397..d16c1a85 100644 --- a/crates/prometeu-bytecode/src/abi.rs +++ b/crates/prometeu-bytecode/src/abi.rs @@ -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), diff --git a/crates/prometeu-bytecode/src/opcode.rs b/crates/prometeu-bytecode/src/opcode.rs index efe010f6..f8ae69f0 100644 --- a/crates/prometeu-bytecode/src/opcode.rs +++ b/crates/prometeu-bytecode/src/opcode.rs @@ -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 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, diff --git a/crates/prometeu-core/src/virtual_machine/opcode_spec.rs b/crates/prometeu-core/src/virtual_machine/opcode_spec.rs index 6c42029c..a26fd50b 100644 --- a/crates/prometeu-core/src/virtual_machine/opcode_spec.rs +++ b/crates/prometeu-core/src/virtual_machine/opcode_spec.rs @@ -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 }, diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 34f14f17..5a4b69c2 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -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(&mut self, f: F) -> Result<(), String> + fn binary_op(&mut self, f: F) -> Result<(), LogicalFrameEndingReason> where - F: FnOnce(Value, Value) -> Result, + F: FnOnce(Value, Value) -> Result, { - 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(); diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index 198babd7..0c7c05ca 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -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: