This commit is contained in:
Nilton Constantino 2026-01-31 18:12:44 +00:00
parent 9d484a9636
commit 33908aa828
No known key found for this signature in database
4 changed files with 283 additions and 84 deletions

View File

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

View File

@ -686,6 +686,7 @@ mod tests {
rom: vec![
0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0
0x11, 0x00, // Pop
0x51, 0x00 // Ret
],
}).unwrap();

View File

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

View File

@ -1,77 +1,3 @@
## PR-07 — Calling convention v0: CALL / RET / multi-slot returns
**Why:** Without a correct call model, PBS isnt executable.
### Scope
* Introduce `CALL <u16 func_id>`
* 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 <u16 id> <u8 arg_slots> <u8 ret_slots>`
* 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.