solidify VM model about Call/Ret - PushScope/PopScope
This commit is contained in:
parent
0ea26f4efd
commit
1ce6a471c0
@ -1,5 +1,4 @@
|
|||||||
pub struct CallFrame {
|
pub struct CallFrame {
|
||||||
pub return_address: usize,
|
pub return_pc: u32,
|
||||||
pub stack_base: usize,
|
pub stack_base: usize,
|
||||||
pub locals_count: usize,
|
|
||||||
}
|
}
|
||||||
@ -2,6 +2,7 @@ mod virtual_machine;
|
|||||||
mod value;
|
mod value;
|
||||||
mod opcode;
|
mod opcode;
|
||||||
mod call_frame;
|
mod call_frame;
|
||||||
|
mod scope_frame;
|
||||||
mod program;
|
mod program;
|
||||||
pub mod native_interface;
|
pub mod native_interface;
|
||||||
|
|
||||||
|
|||||||
3
crates/prometeu-core/src/virtual_machine/scope_frame.rs
Normal file
3
crates/prometeu-core/src/virtual_machine/scope_frame.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub struct ScopeFrame {
|
||||||
|
pub scope_stack_base: usize,
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::hardware::HardwareBridge;
|
use crate::hardware::HardwareBridge;
|
||||||
use crate::prometeu_os::NativeInterface;
|
use crate::prometeu_os::NativeInterface;
|
||||||
use crate::virtual_machine::call_frame::CallFrame;
|
use crate::virtual_machine::call_frame::CallFrame;
|
||||||
|
use crate::virtual_machine::scope_frame::ScopeFrame;
|
||||||
use crate::virtual_machine::opcode::OpCode;
|
use crate::virtual_machine::opcode::OpCode;
|
||||||
use crate::virtual_machine::value::Value;
|
use crate::virtual_machine::value::Value;
|
||||||
use crate::virtual_machine::Program;
|
use crate::virtual_machine::Program;
|
||||||
@ -42,8 +43,10 @@ pub struct VirtualMachine {
|
|||||||
pub pc: usize,
|
pub pc: usize,
|
||||||
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
|
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
|
||||||
pub operand_stack: Vec<Value>,
|
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>,
|
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.
|
/// Globals: storage for persistent variables that survive between frames.
|
||||||
pub globals: Vec<Value>,
|
pub globals: Vec<Value>,
|
||||||
/// The currently loaded program (Bytecode + Constant Pool).
|
/// The currently loaded program (Bytecode + Constant Pool).
|
||||||
@ -65,6 +68,7 @@ impl VirtualMachine {
|
|||||||
pc: 0,
|
pc: 0,
|
||||||
operand_stack: Vec::new(),
|
operand_stack: Vec::new(),
|
||||||
call_stack: Vec::new(),
|
call_stack: Vec::new(),
|
||||||
|
scope_stack: Vec::new(),
|
||||||
globals: Vec::new(),
|
globals: Vec::new(),
|
||||||
program: Program::new(rom, constant_pool),
|
program: Program::new(rom, constant_pool),
|
||||||
heap: Vec::new(),
|
heap: Vec::new(),
|
||||||
@ -101,6 +105,7 @@ impl VirtualMachine {
|
|||||||
// Full state reset to ensure a clean start for the App
|
// Full state reset to ensure a clean start for the App
|
||||||
self.operand_stack.clear();
|
self.operand_stack.clear();
|
||||||
self.call_stack.clear();
|
self.call_stack.clear();
|
||||||
|
self.scope_stack.clear();
|
||||||
self.globals.clear();
|
self.globals.clear();
|
||||||
self.heap.clear();
|
self.heap.clear();
|
||||||
self.cycles = 0;
|
self.cycles = 0;
|
||||||
@ -449,9 +454,8 @@ impl VirtualMachine {
|
|||||||
let args_count = self.read_u32()? as usize;
|
let args_count = self.read_u32()? as usize;
|
||||||
let stack_base = self.operand_stack.len() - args_count;
|
let stack_base = self.operand_stack.len() - args_count;
|
||||||
self.call_stack.push(CallFrame {
|
self.call_stack.push(CallFrame {
|
||||||
return_address: self.pc,
|
return_pc: self.pc as u32,
|
||||||
stack_base,
|
stack_base,
|
||||||
locals_count: args_count,
|
|
||||||
});
|
});
|
||||||
self.pc = addr;
|
self.pc = addr;
|
||||||
}
|
}
|
||||||
@ -462,24 +466,17 @@ impl VirtualMachine {
|
|||||||
self.operand_stack.truncate(frame.stack_base);
|
self.operand_stack.truncate(frame.stack_base);
|
||||||
// Return the result of the function
|
// Return the result of the function
|
||||||
self.push(return_val);
|
self.push(return_val);
|
||||||
self.pc = frame.return_address;
|
self.pc = frame.return_pc as usize;
|
||||||
}
|
}
|
||||||
OpCode::PushScope => {
|
OpCode::PushScope => {
|
||||||
// Used for blocks within a function that have their own locals
|
// Used for blocks within a function that have their own locals
|
||||||
let locals_count = self.read_u32()? as usize;
|
self.scope_stack.push(ScopeFrame {
|
||||||
let stack_base = self.operand_stack.len();
|
scope_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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
OpCode::PopScope => {
|
OpCode::PopScope => {
|
||||||
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
|
let frame = self.scope_stack.pop().ok_or("Scope stack underflow")?;
|
||||||
self.operand_stack.truncate(frame.stack_base);
|
self.operand_stack.truncate(frame.scope_stack_base);
|
||||||
}
|
}
|
||||||
OpCode::Alloc => {
|
OpCode::Alloc => {
|
||||||
// Allocates 'size' values on the heap and pushes a reference to the stack
|
// 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();
|
vm.step(&mut native, &mut hw).unwrap();
|
||||||
assert_eq!(vm.peek().unwrap(), &Value::String("hello".into()));
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,8 @@ The VM has:
|
|||||||
|
|
||||||
* **PC (Program Counter)** — next instruction
|
* **PC (Program Counter)** — next instruction
|
||||||
* **Operand Stack** — value stack
|
* **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
|
* **Heap** — dynamic memory
|
||||||
* **Globals** — global variables
|
* **Globals** — global variables
|
||||||
* **Constant Pool** — literals and references
|
* **Constant Pool** — literals and references
|
||||||
@ -175,11 +176,11 @@ State:
|
|||||||
### 6.6 Functions
|
### 6.6 Functions
|
||||||
|
|
||||||
| Instruction | Cycles | Description |
|
| Instruction | Cycles | Description |
|
||||||
|----------------| ------ |---------------------------------|
|
|----------------| ------ |--------------------------------------------|
|
||||||
| `CALL addr` | 5 | Call |
|
| `CALL addr` | 5 | Saves PC and creates a new call frame |
|
||||||
| `RET` | 4 | Return |
|
| `RET` | 4 | Returns from function, restoring PC |
|
||||||
| `PUSH_SCOPE n` | 3 | Creates scope (execution frame) |
|
| `PUSH_SCOPE` | 3 | Creates a scope within the current function |
|
||||||
| `POP_SCOPE` | 3 | Removes scope |
|
| `POP_SCOPE` | 3 | Removes current scope and its local variables |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user