From a1fe7cbca9e245b2bddbc3cf7c7db110ba91204f Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 09:12:16 +0000 Subject: [PATCH] improve specs around ABI bytecode contract --- .../src/virtual_machine/native_interface.rs | 7 +++ .../src/virtual_machine/virtual_machine.rs | 48 ++++++++++++++++++- docs/specs/topics/chapter-2.md | 16 +++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/prometeu-core/src/virtual_machine/native_interface.rs b/crates/prometeu-core/src/virtual_machine/native_interface.rs index a5ce0f1f..bd7982c9 100644 --- a/crates/prometeu-core/src/virtual_machine/native_interface.rs +++ b/crates/prometeu-core/src/virtual_machine/native_interface.rs @@ -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; } \ No newline at end of file diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index e56ccf9f..f54c7065 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -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(); diff --git a/docs/specs/topics/chapter-2.md b/docs/specs/topics/chapter-2.md index 0b8a9273..b1226cfc 100644 --- a/docs/specs/topics/chapter-2.md +++ b/docs/specs/topics/chapter-2.md @@ -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 |