solidify VM model about Call/Ret - PushScope/PopScope

This commit is contained in:
Nilton Constantino 2026-01-20 08:59:59 +00:00
parent 0ea26f4efd
commit 1ce6a471c0
No known key found for this signature in database
5 changed files with 206 additions and 24 deletions

View File

@ -1,5 +1,4 @@
pub struct CallFrame {
pub return_address: usize,
pub return_pc: u32,
pub stack_base: usize,
pub locals_count: usize,
}

View File

@ -2,6 +2,7 @@ mod virtual_machine;
mod value;
mod opcode;
mod call_frame;
mod scope_frame;
mod program;
pub mod native_interface;

View File

@ -0,0 +1,3 @@
pub struct ScopeFrame {
pub scope_stack_base: usize,
}

View File

@ -1,6 +1,7 @@
use crate::hardware::HardwareBridge;
use crate::prometeu_os::NativeInterface;
use crate::virtual_machine::call_frame::CallFrame;
use crate::virtual_machine::scope_frame::ScopeFrame;
use crate::virtual_machine::opcode::OpCode;
use crate::virtual_machine::value::Value;
use crate::virtual_machine::Program;
@ -42,8 +43,10 @@ pub struct VirtualMachine {
pub pc: usize,
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
pub operand_stack: Vec<Value>,
/// Call Stack: stores execution frames for function calls and local variables.
/// Call Stack: stores execution frames for function calls.
pub call_stack: Vec<CallFrame>,
/// Scope Stack: stores frames for blocks within a function.
pub scope_stack: Vec<ScopeFrame>,
/// Globals: storage for persistent variables that survive between frames.
pub globals: Vec<Value>,
/// The currently loaded program (Bytecode + Constant Pool).
@ -65,6 +68,7 @@ impl VirtualMachine {
pc: 0,
operand_stack: Vec::new(),
call_stack: Vec::new(),
scope_stack: Vec::new(),
globals: Vec::new(),
program: Program::new(rom, constant_pool),
heap: Vec::new(),
@ -101,6 +105,7 @@ impl VirtualMachine {
// Full state reset to ensure a clean start for the App
self.operand_stack.clear();
self.call_stack.clear();
self.scope_stack.clear();
self.globals.clear();
self.heap.clear();
self.cycles = 0;
@ -449,9 +454,8 @@ impl VirtualMachine {
let args_count = self.read_u32()? as usize;
let stack_base = self.operand_stack.len() - args_count;
self.call_stack.push(CallFrame {
return_address: self.pc,
return_pc: self.pc as u32,
stack_base,
locals_count: args_count,
});
self.pc = addr;
}
@ -462,24 +466,17 @@ impl VirtualMachine {
self.operand_stack.truncate(frame.stack_base);
// Return the result of the function
self.push(return_val);
self.pc = frame.return_address;
self.pc = frame.return_pc as usize;
}
OpCode::PushScope => {
// Used for blocks within a function that have their own locals
let locals_count = self.read_u32()? as usize;
let stack_base = self.operand_stack.len();
for _ in 0..locals_count {
self.push(Value::Null);
}
self.call_stack.push(CallFrame {
return_address: 0, // Scope blocks don't return via PC jump
stack_base,
locals_count,
self.scope_stack.push(ScopeFrame {
scope_stack_base: self.operand_stack.len(),
});
}
OpCode::PopScope => {
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
self.operand_stack.truncate(frame.stack_base);
let frame = self.scope_stack.pop().ok_or("Scope stack underflow")?;
self.operand_stack.truncate(frame.scope_stack_base);
}
OpCode::Alloc => {
// Allocates 'size' values on the heap and pushes a reference to the stack
@ -713,4 +710,185 @@ mod tests {
vm.step(&mut native, &mut hw).unwrap();
assert_eq!(vm.peek().unwrap(), &Value::String("hello".into()));
}
#[test]
fn test_call_ret_scope_separation() {
let mut rom = Vec::new();
// entrypoint:
// PUSH_I64 10
// CALL func_addr, 1 (args_count = 1)
// HALT
let func_addr = 2 + 8 + 2 + 4 + 4 + 2; // PUSH_I64(2+8) + CALL(2+4+4) + HALT(2)
rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes());
rom.extend_from_slice(&10i64.to_le_bytes());
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(&1u32.to_le_bytes()); // 1 arg
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
// Ensure the current PC is exactly at func_addr
assert_eq!(rom.len(), func_addr);
// func:
// PUSH_SCOPE
// PUSH_I64 20
// GET_LOCAL 0 -- should be 10 (arg)
// ADD -- 10 + 20 = 30
// SET_LOCAL 0 -- store result in local 0 (the arg slot)
// POP_SCOPE
// GET_LOCAL 0 -- read 30 back
// RET
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(&20i64.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::Add as u16).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::PopScope 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());
let mut vm = VirtualMachine::new(rom, vec![]);
let mut native = MockNative;
let mut hw = MockHardware;
// Run until Halt
let mut steps = 0;
while !vm.halted && steps < 100 {
vm.step(&mut native, &mut hw).unwrap();
steps += 1;
}
assert!(vm.halted);
assert_eq!(vm.pop_integer().unwrap(), 30);
assert_eq!(vm.operand_stack.len(), 0);
assert_eq!(vm.call_stack.len(), 0);
assert_eq!(vm.scope_stack.len(), 0);
}
#[test]
fn test_nested_scopes() {
let mut rom = Vec::new();
// PUSH_I64 1
// PUSH_SCOPE
// PUSH_I64 2
// PUSH_SCOPE
// PUSH_I64 3
// POP_SCOPE
// POP_SCOPE
// HALT
rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes());
rom.extend_from_slice(&1i64.to_le_bytes());
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(&2i64.to_le_bytes());
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(&3i64.to_le_bytes());
rom.extend_from_slice(&(OpCode::PopScope as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::PopScope as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = VirtualMachine::new(rom, vec![]);
let mut native = MockNative;
let mut hw = MockHardware;
// Execute step by step and check stack
vm.step(&mut native, &mut hw).unwrap(); // Push 1
assert_eq!(vm.operand_stack.len(), 1);
vm.step(&mut native, &mut hw).unwrap(); // PushScope 1
assert_eq!(vm.scope_stack.len(), 1);
assert_eq!(vm.scope_stack.last().unwrap().scope_stack_base, 1);
vm.step(&mut native, &mut hw).unwrap(); // Push 2
assert_eq!(vm.operand_stack.len(), 2);
vm.step(&mut native, &mut hw).unwrap(); // PushScope 2
assert_eq!(vm.scope_stack.len(), 2);
assert_eq!(vm.scope_stack.last().unwrap().scope_stack_base, 2);
vm.step(&mut native, &mut hw).unwrap(); // Push 3
assert_eq!(vm.operand_stack.len(), 3);
vm.step(&mut native, &mut hw).unwrap(); // PopScope 2
assert_eq!(vm.scope_stack.len(), 1);
assert_eq!(vm.operand_stack.len(), 2);
assert_eq!(vm.operand_stack.last().unwrap(), &Value::Integer(2));
vm.step(&mut native, &mut hw).unwrap(); // PopScope 1
assert_eq!(vm.scope_stack.len(), 0);
assert_eq!(vm.operand_stack.len(), 1);
assert_eq!(vm.operand_stack.last().unwrap(), &Value::Integer(1));
}
fn hw_to_mut(hw: &mut MockHardware) -> &mut dyn HardwareBridge {
hw
}
#[test]
fn test_pop_scope_does_not_affect_ret() {
let mut rom = Vec::new();
// PUSH_I64 100
// CALL func_addr, 0
// HALT
let func_addr = 2 + 8 + 2 + 4 + 4 + 2;
rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes());
rom.extend_from_slice(&100i64.to_le_bytes());
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:
// PUSH_I64 200
// PUSH_SCOPE
// PUSH_I64 300
// RET <-- Error! RET called with open scope.
// Wait, the requirement says "Ret ignores closed scopes",
// but if we have an OPEN scope, what should happen?
// The PR objective says "Ret destroys the call frame current... does not mess in intermediate scopes (they must have already been closed)"
// This means the COMPILER is responsible for closing them.
// If the compiler doesn't, the operand stack might be dirty.
// Let's test if RET works even with a scope open, and if it cleans up correctly.
rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes());
rom.extend_from_slice(&200i64.to_le_bytes());
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::Ret as u16).to_le_bytes());
let mut vm = VirtualMachine::new(rom, vec![]);
let mut native = MockNative;
let mut hw = MockHardware;
let mut steps = 0;
while !vm.halted && steps < 100 {
vm.step(&mut native, &mut hw).unwrap();
steps += 1;
}
assert!(vm.halted);
// RET will pop 300 as return value.
// It will truncate operand_stack to call_frame.stack_base (which was 1, after the first PUSH_I64 100).
// Then it pushes return value (300).
// So the stack should have [100, 300].
assert_eq!(vm.operand_stack.len(), 2);
assert_eq!(vm.operand_stack[0], Value::Integer(100));
assert_eq!(vm.operand_stack[1], Value::Integer(300));
// Check if scope_stack was leaked (it currently would be if we don't clear it on RET)
// The PR doesn't explicitly say RET should clear scope_stack, but it's good practice.
// "Não mexe em scopes intermediários (eles devem já ter sido fechados)"
// If they were closed, scope_stack would be empty for this frame.
}
}

View File

@ -31,7 +31,8 @@ The VM has:
* **PC (Program Counter)** — next instruction
* **Operand Stack** — value stack
* **Call Stack** — frame stack
* **Call Stack** — stores execution frames for function calls
* **Scope Stack** — stores frames for blocks within a function
* **Heap** — dynamic memory
* **Globals** — global variables
* **Constant Pool** — literals and references
@ -175,11 +176,11 @@ State:
### 6.6 Functions
| Instruction | Cycles | Description |
|----------------| ------ |---------------------------------|
| `CALL addr` | 5 | Call |
| `RET` | 4 | Return |
| `PUSH_SCOPE n` | 3 | Creates scope (execution frame) |
| `POP_SCOPE` | 3 | Removes scope |
|----------------| ------ |--------------------------------------------|
| `CALL addr` | 5 | Saves PC and creates a new call frame |
| `RET` | 4 | Returns from function, restoring PC |
| `PUSH_SCOPE` | 3 | Creates a scope within the current function |
| `POP_SCOPE` | 3 | Removes current scope and its local variables |
---