This commit is contained in:
Nilton Constantino 2026-01-31 17:36:26 +00:00
parent e784dab34e
commit 13e58a8efd
No known key found for this signature in database
5 changed files with 208 additions and 43 deletions

View File

@ -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),

View File

@ -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,
})
}
}

View File

@ -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;

View File

@ -28,6 +28,12 @@ pub enum LogicalFrameEndingReason {
Panic(String),
}
impl From<TrapInfo> 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),
}
}
}

View File

@ -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 <u16 slot>` pushes value slots
* `SET_LOCAL <u16 slot>` pops value slots and writes
* `INIT_LOCAL <u16 slot>` (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 doesnt corrupt stack.