diff --git a/crates/prometeu-core/src/virtual_machine/call_frame.rs b/crates/prometeu-core/src/virtual_machine/call_frame.rs index 6918cb4f..afb68271 100644 --- a/crates/prometeu-core/src/virtual_machine/call_frame.rs +++ b/crates/prometeu-core/src/virtual_machine/call_frame.rs @@ -1,5 +1,4 @@ pub struct CallFrame { - pub return_address: usize, + pub return_pc: u32, pub stack_base: usize, - pub locals_count: usize, } \ No newline at end of file diff --git a/crates/prometeu-core/src/virtual_machine/mod.rs b/crates/prometeu-core/src/virtual_machine/mod.rs index ac6b269f..dccbfb80 100644 --- a/crates/prometeu-core/src/virtual_machine/mod.rs +++ b/crates/prometeu-core/src/virtual_machine/mod.rs @@ -2,6 +2,7 @@ mod virtual_machine; mod value; mod opcode; mod call_frame; +mod scope_frame; mod program; pub mod native_interface; diff --git a/crates/prometeu-core/src/virtual_machine/scope_frame.rs b/crates/prometeu-core/src/virtual_machine/scope_frame.rs new file mode 100644 index 00000000..e0d927bb --- /dev/null +++ b/crates/prometeu-core/src/virtual_machine/scope_frame.rs @@ -0,0 +1,3 @@ +pub struct ScopeFrame { + pub scope_stack_base: usize, +} \ 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 54ca5a89..e56ccf9f 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -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, - /// Call Stack: stores execution frames for function calls and local variables. + /// Call Stack: stores execution frames for function calls. pub call_stack: Vec, + /// Scope Stack: stores frames for blocks within a function. + pub scope_stack: Vec, /// Globals: storage for persistent variables that survive between frames. pub globals: Vec, /// 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. + } } diff --git a/docs/specs/topics/chapter-2.md b/docs/specs/topics/chapter-2.md index f1c4d97a..0b8a9273 100644 --- a/docs/specs/topics/chapter-2.md +++ b/docs/specs/topics/chapter-2.md @@ -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 @@ -174,12 +175,12 @@ 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 | +| Instruction | Cycles | Description | +|----------------| ------ |--------------------------------------------| +| `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 | ---