pr 44
This commit is contained in:
parent
e784dab34e
commit
13e58a8efd
@ -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),
|
||||
|
||||
29
crates/prometeu-core/src/virtual_machine/local_addressing.rs
Normal file
29
crates/prometeu-core/src/virtual_machine/local_addressing.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 doesn’t corrupt stack.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user