diff --git a/crates/prometeu-bytecode/src/abi.rs b/crates/prometeu-bytecode/src/abi.rs index d16c1a85..6feb351a 100644 --- a/crates/prometeu-bytecode/src/abi.rs +++ b/crates/prometeu-bytecode/src/abi.rs @@ -45,6 +45,10 @@ pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008; pub const TRAP_INVALID_LOCAL: u32 = 0x0000_0009; /// Division or modulo by zero. pub const TRAP_DIV_ZERO: u32 = 0x0000_000A; +/// Attempted to call a function that does not exist in the function table. +pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B; +/// Executed RET with an incorrect stack height (mismatch with function metadata). +pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C; /// Detailed information about a runtime trap. #[derive(Debug, Clone, PartialEq, Eq)] @@ -87,6 +91,8 @@ mod tests { assert_eq!(TRAP_STACK_UNDERFLOW, 0x08); assert_eq!(TRAP_INVALID_LOCAL, 0x09); assert_eq!(TRAP_DIV_ZERO, 0x0A); + assert_eq!(TRAP_INVALID_FUNC, 0x0B); + assert_eq!(TRAP_BAD_RET_SLOTS, 0x0C); } #[test] @@ -104,6 +110,8 @@ System Traps: - STACK_UNDERFLOW (0x08): Missing syscall arguments. - INVALID_LOCAL (0x09): Local slot out of bounds. - DIV_ZERO (0x0A): Division by zero. +- INVALID_FUNC (0x0B): Function table index out of bounds. +- BAD_RET_SLOTS (0x0C): Stack height mismatch at RET. Operand Sizes: - Alloc: 8 bytes (u32 type_id, u32 slots) @@ -114,9 +122,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- 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", + "\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- INVALID_FUNC (0x{:02X}): Function table index out of bounds.\n- BAD_RET_SLOTS (0x{:02X}): Stack height mismatch at RET.\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_DIV_ZERO, + TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW, TRAP_INVALID_LOCAL, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_BAD_RET_SLOTS, operand_size(OpCode::Alloc), operand_size(OpCode::GateLoad), operand_size(OpCode::GateStore), diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index b6c74cb3..e2712238 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -686,6 +686,7 @@ mod tests { rom: vec![ 0x17, 0x00, // PushI32 0x00, 0x00, 0x00, 0x00, // value 0 + 0x11, 0x00, // Pop 0x51, 0x00 // Ret ], }).unwrap(); diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 7717c3fa..a4535152 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, TRAP_DIV_ZERO, TRAP_TYPE}; +use prometeu_bytecode::abi::{TrapInfo, TRAP_OOB, TRAP_DIV_ZERO, TRAP_TYPE, TRAP_INVALID_FUNC, TRAP_BAD_RET_SLOTS}; /// 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 @@ -835,10 +835,20 @@ impl VirtualMachine { } OpCode::Call => { let func_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize; - let callee = self.program.functions.get(func_id).ok_or_else(|| LogicalFrameEndingReason::Panic(format!("Invalid func_id {}", func_id)))?; + let callee = self.program.functions.get(func_id).ok_or_else(|| { + LogicalFrameEndingReason::Trap(TrapInfo { + code: TRAP_INVALID_FUNC, + opcode: opcode as u16, + message: format!("Invalid func_id {}", func_id), + pc: start_pc as u32, + }) + })?; if self.operand_stack.len() < callee.param_slots as usize { - return Err(LogicalFrameEndingReason::Panic("Stack underflow during CALL: not enough arguments".into())); + return Err(LogicalFrameEndingReason::Panic(format!( + "Stack underflow during CALL to func {}: expected at least {} arguments, got {}", + func_id, callee.param_slots, self.operand_stack.len() + ))); } let stack_base = self.operand_stack.len() - callee.param_slots as usize; @@ -860,7 +870,22 @@ impl VirtualMachine { let func = &self.program.functions[frame.func_idx]; let return_slots = func.return_slots as usize; - // Copy return values + let current_height = self.operand_stack.len(); + let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots; + + if current_height != expected_height { + return Err(LogicalFrameEndingReason::Trap(TrapInfo { + code: TRAP_BAD_RET_SLOTS, + opcode: opcode as u16, + message: format!( + "Incorrect stack height at RET in func {}: expected {} slots (stack_base={} + params={} + locals={} + returns={}), got {}", + frame.func_idx, expected_height, frame.stack_base, func.param_slots, func.local_slots, return_slots, current_height + ), + pc: start_pc as u32, + })); + } + + // Copy return values (preserving order: pop return_slots values, then reverse to push back) let mut return_vals = Vec::with_capacity(return_slots); for _ in 0..return_slots { return_vals.push(self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?); @@ -1355,8 +1380,10 @@ mod tests { let res = vm.step(&mut native, &mut hw); // RET -> should fail assert!(res.is_err()); match res.unwrap_err() { - LogicalFrameEndingReason::Panic(msg) => assert!(msg.contains("Stack underflow")), - _ => panic!("Expected Panic"), + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, TRAP_BAD_RET_SLOTS); + } + _ => panic!("Expected Trap(TRAP_BAD_RET_SLOTS)"), } // Agora com valor de retorno @@ -1475,6 +1502,7 @@ mod tests { rom.extend_from_slice(&(OpCode::PushScope as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); rom.extend_from_slice(&300i64.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PopScope as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let functions = vec![ @@ -1505,7 +1533,7 @@ mod tests { assert!(vm.halted); assert_eq!(vm.operand_stack.len(), 2); assert_eq!(vm.operand_stack[0], Value::Int64(100)); - assert_eq!(vm.operand_stack[1], Value::Int64(300)); + assert_eq!(vm.operand_stack[1], Value::Int64(200)); } #[test] @@ -1747,6 +1775,7 @@ mod tests { let rom = vec![ 0x17, 0x00, // PushI32 0x00, 0x00, 0x00, 0x00, // value 0 + 0x11, 0x00, // Pop 0x51, 0x00 // Ret ]; let mut vm = VirtualMachine::new(rom, vec![]); @@ -2027,6 +2056,240 @@ mod tests { assert!(res.is_ok(), "Should NOT fail if Call pattern is in immediate: {:?}", res); } + #[test] + fn test_calling_convention_add() { + let mut native = MockNative; + let mut hw = MockHardware; + + // F0 (entry): + // PUSH_I32 10 + // PUSH_I32 20 + // CALL 1 (add) + // HALT + // F1 (add): + // GET_LOCAL 0 (a) + // GET_LOCAL 1 (b) + // ADD + // RET (1 slot) + + let mut rom = Vec::new(); + // F0 + let f0_start = 0; + 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::Call as u16).to_le_bytes()); + rom.extend_from_slice(&1u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let f0_len = rom.len() - f0_start; + + // F1 + let f1_start = rom.len() as u32; + rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); + rom.extend_from_slice(&0u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); + rom.extend_from_slice(&1u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); + let f1_len = rom.len() as u32 - f1_start; + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.program.functions = std::sync::Arc::from(vec![ + FunctionMeta { + code_offset: f0_start as u32, + code_len: f0_len as u32, + ..Default::default() + }, + FunctionMeta { + code_offset: f1_start, + code_len: f1_len, + param_slots: 2, + return_slots: 1, + ..Default::default() + }, + ]); + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + assert_eq!(report.reason, LogicalFrameEndingReason::Halted); + assert_eq!(vm.operand_stack.last().unwrap(), &Value::Int32(30)); + } + + #[test] + fn test_calling_convention_multi_slot_return() { + let mut native = MockNative; + let mut hw = MockHardware; + + // F0: + // CALL 1 + // HALT + // F1: + // PUSH_I32 100 + // PUSH_I32 200 + // RET (2 slots) + + let mut rom = Vec::new(); + // F0 + let f0_start = 0; + rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); + rom.extend_from_slice(&1u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let f0_len = rom.len() - f0_start; + + // F1 + let f1_start = rom.len() as u32; + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&100i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&200i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); + let f1_len = rom.len() as u32 - f1_start; + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.program.functions = std::sync::Arc::from(vec![ + FunctionMeta { + code_offset: f0_start as u32, + code_len: f0_len as u32, + ..Default::default() + }, + FunctionMeta { + code_offset: f1_start, + code_len: f1_len, + param_slots: 0, + return_slots: 2, + ..Default::default() + }, + ]); + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + assert_eq!(report.reason, LogicalFrameEndingReason::Halted); + // Stack should be [100, 200] + assert_eq!(vm.operand_stack.len(), 2); + assert_eq!(vm.operand_stack[0], Value::Int32(100)); + assert_eq!(vm.operand_stack[1], Value::Int32(200)); + } + + #[test] + fn test_calling_convention_void_call() { + let mut native = MockNative; + let mut hw = MockHardware; + + // F0: + // PUSH_I32 42 + // CALL 1 + // HALT + // F1: + // POP + // RET (0 slots) + + let mut rom = Vec::new(); + let f0_start = 0; + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); + rom.extend_from_slice(&1u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let f0_len = rom.len() - f0_start; + + let f1_start = rom.len() as u32; + rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); + let f1_len = rom.len() as u32 - f1_start; + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.program.functions = std::sync::Arc::from(vec![ + FunctionMeta { + code_offset: f0_start as u32, + code_len: f0_len as u32, + ..Default::default() + }, + FunctionMeta { + code_offset: f1_start, + code_len: f1_len, + param_slots: 1, + return_slots: 0, + ..Default::default() + }, + ]); + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + assert_eq!(report.reason, LogicalFrameEndingReason::Halted); + assert_eq!(vm.operand_stack.len(), 0); + } + + #[test] + fn test_trap_invalid_func() { + let mut native = MockNative; + let mut hw = MockHardware; + + // CALL 99 (invalid) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); + rom.extend_from_slice(&99u32.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_INVALID_FUNC); + assert_eq!(trap.opcode, OpCode::Call as u16); + } + _ => panic!("Expected Trap(TRAP_INVALID_FUNC), got {:?}", report.reason), + } + } + + #[test] + fn test_trap_bad_ret_slots() { + let mut native = MockNative; + let mut hw = MockHardware; + + // F0: CALL 1; HALT + // F1: PUSH_I32 42; RET (expected 0 slots) + + let mut rom = Vec::new(); + let f0_start = 0; + rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes()); + rom.extend_from_slice(&1u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + let f0_len = rom.len() - f0_start; + + let f1_start = rom.len() as u32; + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); + let f1_len = rom.len() as u32 - f1_start; + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.program.functions = std::sync::Arc::from(vec![ + FunctionMeta { + code_offset: f0_start as u32, + code_len: f0_len as u32, + ..Default::default() + }, + FunctionMeta { + code_offset: f1_start, + code_len: f1_len, + param_slots: 0, + return_slots: 0, // ERROR: function pushes 42 but returns 0 + ..Default::default() + }, + ]); + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + match report.reason { + LogicalFrameEndingReason::Trap(trap) => { + assert_eq!(trap.code, TRAP_BAD_RET_SLOTS); + assert_eq!(trap.opcode, OpCode::Ret as u16); + assert!(trap.message.contains("Incorrect stack height")); + } + _ => panic!("Expected Trap(TRAP_BAD_RET_SLOTS), got {:?}", report.reason), + } + } #[test] fn test_locals_round_trip() { let mut native = MockNative; @@ -2044,6 +2307,7 @@ mod tests { rom.extend_from_slice(&0u32.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::Pop as u16).to_le_bytes()); rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes()); rom.extend_from_slice(&0u32.to_le_bytes()); rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); @@ -2051,7 +2315,7 @@ mod tests { let mut vm = VirtualMachine::new(rom, vec![]); vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: 0, - code_len: 18, + code_len: 20, local_slots: 1, return_slots: 1, ..Default::default() diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index f03c9726..c63d44aa 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -1,77 +1,3 @@ -## PR-07 — Calling convention v0: CALL / RET / multi-slot returns - -**Why:** Without a correct call model, PBS isn’t executable. - -### Scope - -* Introduce `CALL ` - - * caller pushes args (slots) - * callee frame allocates locals -* Introduce `RET` - - * callee must leave exactly `return_slots` on operand stack at `RET` - * VM pops frame and transfers return slots to caller -* Define return mechanics for `void` (`return_slots=0`) - -### Deliverables - -* `FunctionTable` indexing and bounds checks -* Deterministic traps: - - * `TRAP_INVALID_FUNC` - * `TRAP_BAD_RET_SLOTS` - -### Tests - -* `fn add(a:int,b:int):int { return a+b; }` -* multi-slot return (e.g., `Pad` flattened) -* void call - -### Acceptance - -* Calls are stable and stack-clean. - ---- - -## PR-08 — Host syscalls v0: stable ABI, multi-slot args/returns - -**Why:** PBS relies on deterministic syscalls; ABI must be frozen and enforced. - -### Scope - -* Unify syscall invocation opcode: - - * `SYSCALL ` -* Runtime validates: - - * pops `arg_slots` - * pushes `ret_slots` -* Implement/confirm: - - * `GfxClear565 (0x1010)` - * `InputPadSnapshot (0x2010)` - * `InputTouchSnapshot (0x2011)` - -### Deliverables - -* A `SyscallRegistry` mapping id -> handler + signature -* Deterministic traps: - - * `TRAP_INVALID_SYSCALL` - * `TRAP_SYSCALL_SIG_MISMATCH` - -### Tests - -* syscall isolated tests -* wrong signature traps - -### Acceptance - -* Syscalls are “industrial”: typed by signature, deterministic, no host surprises. - ---- - ## PR-09 — Debug info v0: spans, symbols, and traceable traps **Why:** Industrial debugging requires actionable failures.