diff --git a/crates/prometeu-bytecode/src/abi.rs b/crates/prometeu-bytecode/src/abi.rs index 570e78e1..f5bbd397 100644 --- a/crates/prometeu-bytecode/src/abi.rs +++ b/crates/prometeu-bytecode/src/abi.rs @@ -19,7 +19,7 @@ pub fn operand_size(opcode: OpCode) -> usize { OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => 4, OpCode::GetGlobal | OpCode::SetGlobal => 4, OpCode::GetLocal | OpCode::SetLocal => 4, - OpCode::Call => 8, // addr(u32) + args_count(u32) + OpCode::Call => 4, // func_id(u32) OpCode::Syscall => 4, OpCode::Alloc => 8, // type_id(u32) + slots(u32) OpCode::GateLoad | OpCode::GateStore => 4, // offset(u32) @@ -41,6 +41,8 @@ pub const TRAP_TYPE: u32 = 0x04; pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007; /// Not enough arguments on the stack for the requested syscall. 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; /// Detailed information about a runtime trap. #[derive(Debug, Clone, PartialEq, Eq)] @@ -81,6 +83,7 @@ mod tests { assert_eq!(TRAP_TYPE, 0x04); assert_eq!(TRAP_INVALID_SYSCALL, 0x07); assert_eq!(TRAP_STACK_UNDERFLOW, 0x08); + assert_eq!(TRAP_INVALID_LOCAL, 0x09); } #[test] @@ -96,6 +99,7 @@ HIP Traps: System Traps: - INVALID_SYSCALL (0x07): Unknown syscall ID. - STACK_UNDERFLOW (0x08): Missing syscall arguments. +- INVALID_LOCAL (0x09): Local slot out of bounds. Operand Sizes: - Alloc: 8 bytes (u32 type_id, u32 slots) @@ -106,9 +110,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\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\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_SYSCALL, TRAP_STACK_UNDERFLOW, TRAP_INVALID_LOCAL, operand_size(OpCode::Alloc), operand_size(OpCode::GateLoad), operand_size(OpCode::GateStore), diff --git a/crates/prometeu-core/src/virtual_machine/local_addressing.rs b/crates/prometeu-core/src/virtual_machine/local_addressing.rs new file mode 100644 index 00000000..d41545df --- /dev/null +++ b/crates/prometeu-core/src/virtual_machine/local_addressing.rs @@ -0,0 +1,29 @@ +use crate::virtual_machine::call_frame::CallFrame; +use prometeu_bytecode::v0::FunctionMeta; +use prometeu_bytecode::abi::{TrapInfo, TRAP_INVALID_LOCAL}; + +/// Computes the absolute stack index for the start of the current frame's locals (including args). +pub fn local_base(frame: &CallFrame) -> usize { + frame.stack_base +} + +/// Computes the absolute stack index for a given local slot. +pub fn local_index(frame: &CallFrame, slot: u32) -> usize { + frame.stack_base + slot as usize +} + +/// Validates that a local slot index is within the valid range for the function. +/// Range: 0 <= slot < (param_slots + local_slots) +pub fn check_local_slot(meta: &FunctionMeta, slot: u32, opcode: u16, pc: u32) -> Result<(), TrapInfo> { + let limit = meta.param_slots as u32 + meta.local_slots as u32; + if slot < limit { + Ok(()) + } else { + Err(TrapInfo { + code: TRAP_INVALID_LOCAL, + opcode, + message: format!("Local slot {} out of bounds for function (limit {})", slot, limit), + pc, + }) + } +} diff --git a/crates/prometeu-core/src/virtual_machine/mod.rs b/crates/prometeu-core/src/virtual_machine/mod.rs index 92e3d2d7..7a7e47e7 100644 --- a/crates/prometeu-core/src/virtual_machine/mod.rs +++ b/crates/prometeu-core/src/virtual_machine/mod.rs @@ -3,6 +3,7 @@ mod value; mod call_frame; mod scope_frame; mod program; +pub mod local_addressing; pub mod opcode_spec; pub mod bytecode; pub mod verifier; diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 73b61911..34f14f17 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -28,6 +28,12 @@ pub enum LogicalFrameEndingReason { Panic(String), } +impl From for LogicalFrameEndingReason { + fn from(info: TrapInfo) -> Self { + LogicalFrameEndingReason::Trap(info) + } +} + /// A report detailing the results of an execution slice (run_budget). #[derive(Debug, Clone, PartialEq, Eq)] pub struct BudgetReport { @@ -201,6 +207,15 @@ impl VirtualMachine { self.operand_stack.clear(); self.call_stack.clear(); self.scope_stack.clear(); + + // Entrypoint also needs locals allocated. + // For the sentinel frame, stack_base is always 0. + if let Some(func) = self.program.functions.get(func_idx) { + for _ in 0..func.local_slots { + self.operand_stack.push(Value::Null); + } + } + self.call_stack.push(CallFrame { return_pc: self.program.rom.len() as u32, stack_base: 0, @@ -606,26 +621,42 @@ impl VirtualMachine { self.globals[idx] = val; } OpCode::GetLocal => { - let idx = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize; + let slot = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()); let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?; - let val = self.operand_stack.get(frame.stack_base + idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid local index".into()))?; + let func = &self.program.functions[frame.func_idx]; + + crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)?; + + let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot); + let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?; self.push(val); } OpCode::SetLocal => { - let idx = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize; + let slot = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()); let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?; - let stack_idx = frame.stack_base + idx; - if stack_idx >= self.operand_stack.len() { - return Err(LogicalFrameEndingReason::Panic("Local index out of bounds".into())); - } + let func = &self.program.functions[frame.func_idx]; + + crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)?; + + let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot); self.operand_stack[stack_idx] = val; } 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)))?; + if self.operand_stack.len() < callee.param_slots as usize { + return Err(LogicalFrameEndingReason::Panic("Stack underflow during CALL: not enough arguments".into())); + } + let stack_base = self.operand_stack.len() - callee.param_slots as usize; + + // Allocate and zero-init local_slots + for _ in 0..callee.local_slots { + self.operand_stack.push(Value::Null); + } + self.call_stack.push(CallFrame { return_pc: self.pc as u32, stack_base, @@ -1682,4 +1713,137 @@ mod tests { let res = vm.initialize(pbc, ""); assert!(res.is_ok(), "Should NOT fail if Call pattern is in immediate: {:?}", res); } + + #[test] + fn test_locals_round_trip() { + let mut native = MockNative; + let mut hw = MockHardware; + + // PUSH_I32 42 + // SET_LOCAL 0 + // PUSH_I32 0 (garbage) + // GET_LOCAL 0 + // RET (1 slot) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes()); + 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::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()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { + code_offset: 0, + code_len: 18, + local_slots: 1, + 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::EndOfRom); + // RET pops return values and pushes them back on the caller stack (which is the sentinel frame's stack here). + assert_eq!(vm.operand_stack, vec![Value::Int32(42)]); + } + + #[test] + fn test_locals_per_call_isolation() { + let mut native = MockNative; + let mut hw = MockHardware; + + // Function 0 (entry): + // CALL 1 + // POP + // CALL 1 + // HALT + // Function 1: + // GET_LOCAL 0 (should be Null initially) + // PUSH_I32 42 + // SET_LOCAL 0 + // RET (1 slot: the initial Null) + + 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::Pop as u16).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::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes()); + rom.extend_from_slice(&0u32.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, + local_slots: 1, + 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); + + // The last value on stack is the return of the second CALL 1, + // which should be Value::Null because locals are zero-initialized on each call. + assert_eq!(vm.operand_stack.last().unwrap(), &Value::Null); + } + + #[test] + fn test_invalid_local_index_traps() { + let mut native = MockNative; + let mut hw = MockHardware; + + // Function with 0 params, 1 local. + // GET_LOCAL 1 (OOB) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::GetLocal 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 mut vm = VirtualMachine::new(rom, vec![]); + vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { + code_offset: 0, + code_len: 8, + local_slots: 1, + ..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, prometeu_bytecode::abi::TRAP_INVALID_LOCAL); + assert_eq!(trap.opcode, OpCode::GetLocal as u16); + assert!(trap.message.contains("out of bounds")); + } + _ => panic!("Expected Trap, got {:?}", report.reason), + } + } } diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index 232f8b4a..198babd7 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -1,36 +1,3 @@ -## PR-04 — Locals opcodes: GET_LOCAL / SET_LOCAL / INIT_LOCAL - -**Why:** PBS `let` and parameters need first-class support. - -### Scope - -* Implement opcodes: - - * `GET_LOCAL ` pushes value slots - * `SET_LOCAL ` pops value slots and writes - * `INIT_LOCAL ` (optional) for explicit initialization semantics -* Enforce bounds: local slot index must be within `[0..param+local_slots)` -* Enforce slot width: if types are multi-slot, compiler emits multiple GET/SET or uses `*_N` variants. - -### Deliverables - -* `LocalAddressing` utilities -* Deterministic trap codes: - - * `TRAP_INVALID_LOCAL` - * `TRAP_LOCAL_WIDTH_MISMATCH` (if enforced) - -### Tests - -* `let x: int = 1; return x;` works -* invalid local index traps - -### Acceptance - -* `let` works reliably; no stack side effects beyond specified pops/pushes. - ---- - ## PR-05 — Core arithmetic + comparisons in VM (int/bounded/bool) **Why:** The minimal executable PBS needs arithmetic that doesn’t corrupt stack.