improve specs around ABI bytecode contract

This commit is contained in:
Nilton Constantino 2026-01-20 09:12:16 +00:00
parent 1ce6a471c0
commit a1fe7cbca9
No known key found for this signature in database
3 changed files with 70 additions and 1 deletions

View File

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

View File

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

View File

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