improve specs around ABI bytecode contract
This commit is contained in:
parent
1ce6a471c0
commit
a1fe7cbca9
@ -2,5 +2,12 @@ use crate::hardware::HardwareBridge;
|
||||
use crate::virtual_machine::VirtualMachine;
|
||||
|
||||
pub trait NativeInterface {
|
||||
/// Dispatches a syscall from the Virtual Machine to the native implementation.
|
||||
///
|
||||
/// ABI Rule: Arguments for the syscall are expected on the `operand_stack` in call order.
|
||||
/// Since the stack is LIFO, the last argument of the call is the first to be popped.
|
||||
///
|
||||
/// The implementation MUST pop all its arguments and SHOULD push a return value if the
|
||||
/// syscall is defined to return one.
|
||||
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<u64, String>;
|
||||
}
|
||||
@ -461,6 +461,8 @@ impl VirtualMachine {
|
||||
}
|
||||
OpCode::Ret => {
|
||||
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
|
||||
// ABI Rule: Every function MUST leave exactly one value on the stack before RET.
|
||||
// This value is popped before cleaning the stack and re-pushed after.
|
||||
let return_val = self.pop()?;
|
||||
// Clean up the operand stack, removing the frame's locals
|
||||
self.operand_stack.truncate(frame.stack_base);
|
||||
@ -513,7 +515,10 @@ impl VirtualMachine {
|
||||
}
|
||||
}
|
||||
OpCode::Syscall => {
|
||||
// Calls a native function implemented by the Firmware/OS
|
||||
// Calls a native function implemented by the Firmware/OS.
|
||||
// ABI Rule: Arguments are pushed in call order (LIFO).
|
||||
// The native implementation is responsible for popping all arguments
|
||||
// and pushing a return value if applicable.
|
||||
let id = self.read_u32()?;
|
||||
let native_cycles = native.syscall(id, self, hw).map_err(|e| format!("syscall 0x{:08X} failed: {}", id, e))?;
|
||||
self.cycles += native_cycles;
|
||||
@ -771,6 +776,47 @@ mod tests {
|
||||
assert_eq!(vm.scope_stack.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ret_mandatory_value() {
|
||||
let mut rom = Vec::new();
|
||||
// entrypoint: CALL func, 0; HALT
|
||||
let func_addr = (2 + 4 + 4) + 2;
|
||||
rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&(func_addr as u32).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes()); // 0 args
|
||||
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
// func: RET (SEM VALOR ANTES)
|
||||
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||
|
||||
let mut vm = VirtualMachine::new(rom, vec![]);
|
||||
let mut native = MockNative;
|
||||
let mut hw = MockHardware;
|
||||
|
||||
vm.step(&mut native, &mut hw).unwrap(); // CALL
|
||||
let res = vm.step(&mut native, &mut hw); // RET -> should fail
|
||||
assert!(res.is_err());
|
||||
assert!(res.unwrap_err().contains("Stack underflow"));
|
||||
|
||||
// Agora com valor de retorno
|
||||
let mut rom2 = Vec::new();
|
||||
rom2.extend_from_slice(&(OpCode::Call as u16).to_le_bytes());
|
||||
rom2.extend_from_slice(&(func_addr as u32).to_le_bytes());
|
||||
rom2.extend_from_slice(&0u32.to_le_bytes());
|
||||
rom2.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
rom2.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes());
|
||||
rom2.extend_from_slice(&123i64.to_le_bytes());
|
||||
rom2.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||
|
||||
let mut vm2 = VirtualMachine::new(rom2, vec![]);
|
||||
vm2.step(&mut native, &mut hw).unwrap(); // CALL
|
||||
vm2.step(&mut native, &mut hw).unwrap(); // PUSH_I64
|
||||
vm2.step(&mut native, &mut hw).unwrap(); // RET
|
||||
|
||||
assert_eq!(vm2.operand_stack.len(), 1);
|
||||
assert_eq!(vm2.pop().unwrap(), Value::Integer(123));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_scopes() {
|
||||
let mut rom = Vec::new();
|
||||
|
||||
@ -182,6 +182,10 @@ State:
|
||||
| `PUSH_SCOPE` | 3 | Creates a scope within the current function |
|
||||
| `POP_SCOPE` | 3 | Removes current scope and its local variables |
|
||||
|
||||
**ABI Rules for Functions:**
|
||||
* **Mandatory Return Value:** Every function MUST leave exactly one value on the stack before `RET`. If the function logic doesn't return a value, it must push `null`.
|
||||
* **Stack Cleanup:** `RET` automatically clears all local variables (based on `stack_base`) and re-pushes the return value.
|
||||
|
||||
---
|
||||
|
||||
### 6.7 Heap
|
||||
@ -206,6 +210,18 @@ Heap is:
|
||||
|--------------| -------- | --------------------- |
|
||||
| `SYSCALL id` | variable | Call to hardware |
|
||||
|
||||
**ABI Rules for Syscalls:**
|
||||
* **Argument Order:** Arguments must be pushed in the order they appear in the call (LIFO stack behavior).
|
||||
* Example: `gfx.draw_rect(x, y, w, h, color)` means:
|
||||
1. `PUSH x`
|
||||
2. `PUSH y`
|
||||
3. `PUSH w`
|
||||
4. `PUSH h`
|
||||
5. `PUSH color`
|
||||
6. `SYSCALL 0x1002`
|
||||
* **Consumption:** The native function MUST pop all its arguments from the stack.
|
||||
* **Return Value:** If the syscall returns a value, it will be pushed onto the stack by the native implementation. If not, the stack state for the caller remains as it was before pushing arguments.
|
||||
|
||||
#### Implemented Syscalls (v0.1)
|
||||
|
||||
| ID | Name | Arguments (Stack) | Return |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user