From 0ea26f4efd22dbfaf06288b1ccb888dbc89d2268 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 08:37:47 +0000 Subject: [PATCH 1/7] add inline opcodes --- .../src/virtual_machine/opcode.rs | 9 ++ .../src/virtual_machine/virtual_machine.rs | 135 ++++++++++++++++++ test-cartridges/color-square/program.pbc | Bin 549 -> 491 bytes 3 files changed, 144 insertions(+) diff --git a/crates/prometeu-core/src/virtual_machine/opcode.rs b/crates/prometeu-core/src/virtual_machine/opcode.rs index 72071448..7e509732 100644 --- a/crates/prometeu-core/src/virtual_machine/opcode.rs +++ b/crates/prometeu-core/src/virtual_machine/opcode.rs @@ -13,6 +13,9 @@ pub enum OpCode { Pop = 0x11, Dup = 0x12, Swap = 0x13, + PushI64 = 0x14, + PushF64 = 0x15, + PushBool = 0x16, // 6.3 Arithmetic Add = 0x20, @@ -64,6 +67,9 @@ impl TryFrom for OpCode { 0x11 => Ok(OpCode::Pop), 0x12 => Ok(OpCode::Dup), 0x13 => Ok(OpCode::Swap), + 0x14 => Ok(OpCode::PushI64), + 0x15 => Ok(OpCode::PushF64), + 0x16 => Ok(OpCode::PushBool), 0x20 => Ok(OpCode::Add), 0x21 => Ok(OpCode::Sub), 0x22 => Ok(OpCode::Mul), @@ -104,6 +110,9 @@ impl OpCode { OpCode::Pop => 1, OpCode::Dup => 1, OpCode::Swap => 1, + OpCode::PushI64 => 2, + OpCode::PushF64 => 2, + OpCode::PushBool => 2, OpCode::Add => 2, OpCode::Sub => 2, OpCode::Mul => 4, diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index c97f371f..54ca5a89 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -312,6 +312,18 @@ impl VirtualMachine { let val = self.program.constant_pool.get(idx).cloned().ok_or("Invalid constant index")?; self.push(val); } + OpCode::PushI64 => { + let val = self.read_i64()?; + self.push(Value::Integer(val)); + } + OpCode::PushF64 => { + let val = self.read_f64()?; + self.push(Value::Float(val)); + } + OpCode::PushBool => { + let val = self.read_u8()?; + self.push(Value::Boolean(val != 0)); + } OpCode::Pop => { self.pop()?; } @@ -534,6 +546,26 @@ impl VirtualMachine { Ok(u32::from_le_bytes(bytes)) } + fn read_i64(&mut self) -> Result { + if self.pc + 8 > self.program.rom.len() { + return Err("Unexpected end of ROM".into()); + } + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&self.program.rom[self.pc..self.pc + 8]); + self.pc += 8; + Ok(i64::from_le_bytes(bytes)) + } + + fn read_f64(&mut self) -> Result { + if self.pc + 8 > self.program.rom.len() { + return Err("Unexpected end of ROM".into()); + } + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&self.program.rom[self.pc..self.pc + 8]); + self.pc += 8; + Ok(f64::from_le_bytes(bytes)) + } + fn read_u16(&mut self) -> Result { if self.pc + 2 > self.program.rom.len() { return Err("Unexpected end of ROM".into()); @@ -546,6 +578,15 @@ impl VirtualMachine { Ok(u16::from_le_bytes(bytes)) } + fn read_u8(&mut self) -> Result { + if self.pc + 1 > self.program.rom.len() { + return Err("Unexpected end of ROM".into()); + } + let val = self.program.rom[self.pc]; + self.pc += 1; + Ok(val) + } + pub fn push(&mut self, val: Value) { self.operand_stack.push(val); } @@ -579,3 +620,97 @@ impl VirtualMachine { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::virtual_machine::Value; + use crate::prometeu_os::NativeInterface; + use crate::hardware::HardwareBridge; + + struct MockNative; + impl NativeInterface for MockNative { + fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine, _hw: &mut dyn HardwareBridge) -> Result { + Ok(0) + } + } + + struct MockHardware; + impl HardwareBridge for MockHardware { + fn gfx(&self) -> &crate::hardware::Gfx { todo!() } + fn gfx_mut(&mut self) -> &mut crate::hardware::Gfx { todo!() } + fn audio(&self) -> &crate::hardware::Audio { todo!() } + fn audio_mut(&mut self) -> &mut crate::hardware::Audio { todo!() } + fn pad(&self) -> &crate::hardware::Pad { todo!() } + fn pad_mut(&mut self) -> &mut crate::hardware::Pad { todo!() } + fn touch(&self) -> &crate::hardware::Touch { todo!() } + fn touch_mut(&mut self) -> &mut crate::hardware::Touch { todo!() } + } + + #[test] + fn test_push_i64_immediate() { + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); + rom.extend_from_slice(&42i64.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; + + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.peek().unwrap(), &Value::Integer(42)); + } + + #[test] + fn test_push_f64_immediate() { + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes()); + rom.extend_from_slice(&3.14f64.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; + + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.peek().unwrap(), &Value::Float(3.14)); + } + + #[test] + fn test_push_bool_immediate() { + let mut rom = Vec::new(); + // True + rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); + rom.push(1); + // False + rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); + rom.push(0); + 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; + + vm.step(&mut native, &mut hw).unwrap(); // Push true + assert_eq!(vm.peek().unwrap(), &Value::Boolean(true)); + vm.step(&mut native, &mut hw).unwrap(); // Push false + assert_eq!(vm.peek().unwrap(), &Value::Boolean(false)); + } + + #[test] + fn test_push_const_string() { + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes()); + rom.extend_from_slice(&0u32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let cp = vec![Value::String("hello".into())]; + let mut vm = VirtualMachine::new(rom, cp); + let mut native = MockNative; + let mut hw = MockHardware; + + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.peek().unwrap(), &Value::String("hello".into())); + } +} diff --git a/test-cartridges/color-square/program.pbc b/test-cartridges/color-square/program.pbc index 972e08a09364985456a0ad06e2027abd8e10f74c..074d114765dad9e1c6e5cf57ebff5c8c397a8956 100644 GIT binary patch literal 491 zcmZXQJr2S!42544p!_Th5pzc7N^Bq{ATjqSJpvLd8wcbnIIdF{?7_0X_*wDm`|Ax9 zp9Tn^#F04%>43~HhcZ%QKofz}Or*rgkP>gm!jPpQD??gC)`n~h*&4F@BOWc??(n9Z z_*gX~%K4R_MccvSGO5&=(X7N%zgr5by#BImdCs;_$&oqhR`POR{C~-Nzx|95odlBH`CH^v2M!kVLa0^5%XU0Lr&xeam`!=@ Date: Tue, 20 Jan 2026 08:59:59 +0000 Subject: [PATCH 2/7] solidify VM model about Call/Ret - PushScope/PopScope --- .../src/virtual_machine/call_frame.rs | 3 +- .../prometeu-core/src/virtual_machine/mod.rs | 1 + .../src/virtual_machine/scope_frame.rs | 3 + .../src/virtual_machine/virtual_machine.rs | 208 ++++++++++++++++-- docs/specs/topics/chapter-2.md | 15 +- 5 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 crates/prometeu-core/src/virtual_machine/scope_frame.rs 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 | --- -- 2.47.2 From a1fe7cbca9e245b2bddbc3cf7c7db110ba91204f Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 09:12:16 +0000 Subject: [PATCH 3/7] 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 | -- 2.47.2 From 6b033f9aa6d3fb55f27583e3800300553559a2c0 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 09:26:12 +0000 Subject: [PATCH 4/7] bytecode extension --- crates/prometeu-core/src/debugger_protocol.rs | 2 +- .../src/prometeu_os/prometeu_os.rs | 18 +- .../src/virtual_machine/opcode.rs | 30 ++ .../src/virtual_machine/value.rs | 67 +++- .../src/virtual_machine/virtual_machine.rs | 323 +++++++++++++++--- docs/specs/topics/chapter-2.md | 23 +- 6 files changed, 398 insertions(+), 65 deletions(-) diff --git a/crates/prometeu-core/src/debugger_protocol.rs b/crates/prometeu-core/src/debugger_protocol.rs index 0eac55dc..0e9b8c89 100644 --- a/crates/prometeu-core/src/debugger_protocol.rs +++ b/crates/prometeu-core/src/debugger_protocol.rs @@ -99,7 +99,7 @@ mod tests { fn test_get_state_serialization() { let resp = DebugResponse::GetState { pc: 42, - stack_top: vec![Value::Integer(10), Value::String("test".into()), Value::Boolean(true)], + stack_top: vec![Value::Int64(10), Value::String("test".into()), Value::Boolean(true)], frame_index: 5, app_id: 1, }; diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index f3f5287b..f216b10b 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -471,7 +471,7 @@ mod tests { os.current_app_id = 123; // 1. Normal log test - vm.push(Value::Integer(2)); // Info + vm.push(Value::Int64(2)); // Info vm.push(Value::String("Hello Log".to_string())); let res = os.syscall(0x5001, &mut vm, &mut hw); assert!(res.is_ok()); @@ -483,7 +483,7 @@ mod tests { // 2. Truncation test let long_msg = "A".repeat(300); - vm.push(Value::Integer(3)); // Warn + vm.push(Value::Int64(3)); // Warn vm.push(Value::String(long_msg)); os.syscall(0x5001, &mut vm, &mut hw).unwrap(); @@ -494,13 +494,13 @@ mod tests { // 3. Rate Limit Test // We already made 2 logs. The limit is 10. for i in 0..8 { - vm.push(Value::Integer(2)); + vm.push(Value::Int64(2)); vm.push(Value::String(format!("Log {}", i))); os.syscall(0x5001, &mut vm, &mut hw).unwrap(); } // The 11th log should be ignored (and generate a system warning) - vm.push(Value::Integer(2)); + vm.push(Value::Int64(2)); vm.push(Value::String("Eleventh log".to_string())); os.syscall(0x5001, &mut vm, &mut hw).unwrap(); @@ -513,7 +513,7 @@ mod tests { // 4. Rate limit reset test in the next frame os.begin_logical_frame(&InputSignals::default(), &mut hw); - vm.push(Value::Integer(2)); + vm.push(Value::Int64(2)); vm.push(Value::String("New frame log".to_string())); os.syscall(0x5001, &mut vm, &mut hw).unwrap(); @@ -521,8 +521,8 @@ mod tests { assert_eq!(recent[0].msg, "New frame log"); // 5. LOG_WRITE_TAG test - vm.push(Value::Integer(2)); // Info - vm.push(Value::Integer(42)); // Tag + vm.push(Value::Int64(2)); // Info + vm.push(Value::Int64(42)); // Tag vm.push(Value::String("Tagged Log".to_string())); os.syscall(0x5002, &mut vm, &mut hw).unwrap(); @@ -670,13 +670,13 @@ impl NativeInterface for PrometeuOS { _ => return Err("Expected string path".into()), }; if self.fs_state != FsState::Mounted { - vm.push(Value::Integer(-1)); + vm.push(Value::Int64(-1)); return Ok(100); } let handle = self.next_handle; self.open_files.insert(handle, path); self.next_handle += 1; - vm.push(Value::Integer(handle as i64)); + vm.push(Value::Int64(handle as i64)); Ok(200) } // FS_READ(handle) -> content diff --git a/crates/prometeu-core/src/virtual_machine/opcode.rs b/crates/prometeu-core/src/virtual_machine/opcode.rs index 7e509732..f71168a1 100644 --- a/crates/prometeu-core/src/virtual_machine/opcode.rs +++ b/crates/prometeu-core/src/virtual_machine/opcode.rs @@ -7,6 +7,7 @@ pub enum OpCode { Halt = 0x01, Jmp = 0x02, JmpIfFalse = 0x03, + JmpIfTrue = 0x04, // 6.2 Stack PushConst = 0x10, @@ -16,6 +17,7 @@ pub enum OpCode { PushI64 = 0x14, PushF64 = 0x15, PushBool = 0x16, + PushI32 = 0x17, // 6.3 Arithmetic Add = 0x20, @@ -31,6 +33,14 @@ pub enum OpCode { And = 0x34, Or = 0x35, Not = 0x36, + BitAnd = 0x37, + BitOr = 0x38, + BitXor = 0x39, + Shl = 0x3A, + Shr = 0x3B, + Lte = 0x3C, + Gte = 0x3D, + Neg = 0x3E, // 6.5 Variables GetGlobal = 0x40, @@ -63,6 +73,7 @@ impl TryFrom for OpCode { 0x01 => Ok(OpCode::Halt), 0x02 => Ok(OpCode::Jmp), 0x03 => Ok(OpCode::JmpIfFalse), + 0x04 => Ok(OpCode::JmpIfTrue), 0x10 => Ok(OpCode::PushConst), 0x11 => Ok(OpCode::Pop), 0x12 => Ok(OpCode::Dup), @@ -70,6 +81,7 @@ impl TryFrom for OpCode { 0x14 => Ok(OpCode::PushI64), 0x15 => Ok(OpCode::PushF64), 0x16 => Ok(OpCode::PushBool), + 0x17 => Ok(OpCode::PushI32), 0x20 => Ok(OpCode::Add), 0x21 => Ok(OpCode::Sub), 0x22 => Ok(OpCode::Mul), @@ -81,6 +93,14 @@ impl TryFrom for OpCode { 0x34 => Ok(OpCode::And), 0x35 => Ok(OpCode::Or), 0x36 => Ok(OpCode::Not), + 0x37 => Ok(OpCode::BitAnd), + 0x38 => Ok(OpCode::BitOr), + 0x39 => Ok(OpCode::BitXor), + 0x3A => Ok(OpCode::Shl), + 0x3B => Ok(OpCode::Shr), + 0x3C => Ok(OpCode::Lte), + 0x3D => Ok(OpCode::Gte), + 0x3E => Ok(OpCode::Neg), 0x40 => Ok(OpCode::GetGlobal), 0x41 => Ok(OpCode::SetGlobal), 0x42 => Ok(OpCode::GetLocal), @@ -106,6 +126,7 @@ impl OpCode { OpCode::Halt => 1, OpCode::Jmp => 2, OpCode::JmpIfFalse => 3, + OpCode::JmpIfTrue => 3, OpCode::PushConst => 2, OpCode::Pop => 1, OpCode::Dup => 1, @@ -113,6 +134,7 @@ impl OpCode { OpCode::PushI64 => 2, OpCode::PushF64 => 2, OpCode::PushBool => 2, + OpCode::PushI32 => 2, OpCode::Add => 2, OpCode::Sub => 2, OpCode::Mul => 4, @@ -124,6 +146,14 @@ impl OpCode { OpCode::And => 2, OpCode::Or => 2, OpCode::Not => 1, + OpCode::BitAnd => 2, + OpCode::BitOr => 2, + OpCode::BitXor => 2, + OpCode::Shl => 2, + OpCode::Shr => 2, + OpCode::Lte => 2, + OpCode::Gte => 2, + OpCode::Neg => 1, OpCode::GetGlobal => 3, OpCode::SetGlobal => 3, OpCode::GetLocal => 2, diff --git a/crates/prometeu-core/src/virtual_machine/value.rs b/crates/prometeu-core/src/virtual_machine/value.rs index 4ecefd85..e1c00655 100644 --- a/crates/prometeu-core/src/virtual_machine/value.rs +++ b/crates/prometeu-core/src/virtual_machine/value.rs @@ -1,9 +1,11 @@ use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Value { - Integer(i64), + Int32(i32), + Int64(i64), Float(f64), Boolean(bool), String(String), @@ -14,10 +16,15 @@ pub enum Value { impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Value::Integer(a), Value::Integer(b)) => a == b, + (Value::Int32(a), Value::Int32(b)) => a == b, + (Value::Int64(a), Value::Int64(b)) => a == b, + (Value::Int32(a), Value::Int64(b)) => *a as i64 == *b, + (Value::Int64(a), Value::Int32(b)) => *a == *b as i64, (Value::Float(a), Value::Float(b)) => a == b, - (Value::Integer(a), Value::Float(b)) => *a as f64 == *b, - (Value::Float(a), Value::Integer(b)) => *a == *b as f64, + (Value::Int32(a), Value::Float(b)) => *a as f64 == *b, + (Value::Float(a), Value::Int32(b)) => *a == *b as f64, + (Value::Int64(a), Value::Float(b)) => *a as f64 == *b, + (Value::Float(a), Value::Int64(b)) => *a == *b as f64, (Value::Boolean(a), Value::Boolean(b)) => a == b, (Value::String(a), Value::String(b)) => a == b, (Value::Ref(a), Value::Ref(b)) => a == b, @@ -27,10 +34,30 @@ impl PartialEq for Value { } } +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Value::Int32(a), Value::Int32(b)) => a.partial_cmp(b), + (Value::Int64(a), Value::Int64(b)) => a.partial_cmp(b), + (Value::Int32(a), Value::Int64(b)) => (*a as i64).partial_cmp(b), + (Value::Int64(a), Value::Int32(b)) => a.partial_cmp(&(*b as i64)), + (Value::Float(a), Value::Float(b)) => a.partial_cmp(b), + (Value::Int32(a), Value::Float(b)) => (*a as f64).partial_cmp(b), + (Value::Float(a), Value::Int32(b)) => a.partial_cmp(&(*b as f64)), + (Value::Int64(a), Value::Float(b)) => (*a as f64).partial_cmp(b), + (Value::Float(a), Value::Int64(b)) => a.partial_cmp(&(*b as f64)), + (Value::Boolean(a), Value::Boolean(b)) => a.partial_cmp(b), + (Value::String(a), Value::String(b)) => a.partial_cmp(b), + _ => None, + } + } +} + impl Value { pub fn as_float(&self) -> Option { match self { - Value::Integer(i) => Some(*i as f64), + Value::Int32(i) => Some(*i as f64), + Value::Int64(i) => Some(*i as f64), Value::Float(f) => Some(*f), _ => None, } @@ -38,7 +65,8 @@ impl Value { pub fn as_integer(&self) -> Option { match self { - Value::Integer(i) => Some(*i), + Value::Int32(i) => Some(*i as i64), + Value::Int64(i) => Some(*i), Value::Float(f) => Some(*f as i64), _ => None, } @@ -51,12 +79,19 @@ mod tests { #[test] fn test_value_equality() { - assert_eq!(Value::Integer(10), Value::Integer(10)); + assert_eq!(Value::Int32(10), Value::Int32(10)); + assert_eq!(Value::Int64(10), Value::Int64(10)); + assert_eq!(Value::Int32(10), Value::Int64(10)); + assert_eq!(Value::Int64(10), Value::Int32(10)); assert_eq!(Value::Float(10.5), Value::Float(10.5)); - assert_eq!(Value::Integer(10), Value::Float(10.0)); - assert_eq!(Value::Float(10.0), Value::Integer(10)); - assert_ne!(Value::Integer(10), Value::Integer(11)); - assert_ne!(Value::Integer(10), Value::Float(10.1)); + assert_eq!(Value::Int32(10), Value::Float(10.0)); + assert_eq!(Value::Float(10.0), Value::Int32(10)); + assert_eq!(Value::Int64(10), Value::Float(10.0)); + assert_eq!(Value::Float(10.0), Value::Int64(10)); + assert_ne!(Value::Int32(10), Value::Int32(11)); + assert_ne!(Value::Int64(10), Value::Int64(11)); + assert_ne!(Value::Int32(10), Value::Int64(11)); + assert_ne!(Value::Int32(10), Value::Float(10.1)); assert_eq!(Value::Boolean(true), Value::Boolean(true)); assert_ne!(Value::Boolean(true), Value::Boolean(false)); assert_eq!(Value::String("oi".into()), Value::String("oi".into())); @@ -65,9 +100,13 @@ mod tests { #[test] fn test_value_conversions() { - let v_int = Value::Integer(42); - assert_eq!(v_int.as_float(), Some(42.0)); - assert_eq!(v_int.as_integer(), Some(42)); + let v_int32 = Value::Int32(42); + assert_eq!(v_int32.as_float(), Some(42.0)); + assert_eq!(v_int32.as_integer(), Some(42)); + + let v_int64 = Value::Int64(42); + assert_eq!(v_int64.as_float(), Some(42.0)); + assert_eq!(v_int64.as_integer(), Some(42)); let v_float = Value::Float(42.7); assert_eq!(v_float.as_float(), Some(42.7)); diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index f54c7065..a6e69de3 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -127,9 +127,9 @@ impl VirtualMachine { cursor += 1; match tag { - 1 => { // Integer (64-bit) + 1 => { // Int64 (64-bit) let val = self.read_i64_at(bytes, &mut cursor)?; - cp.push(Value::Integer(val)); + cp.push(Value::Int64(val)); } 2 => { // Float (64-bit) let val = self.read_f64_at(bytes, &mut cursor)?; @@ -148,6 +148,10 @@ impl VirtualMachine { cursor += len; cp.push(Value::String(s)); } + 5 => { // Int32 (32-bit) + let val = self.read_u32_at(bytes, &mut cursor)? as i32; + cp.push(Value::Int32(val)); + } _ => cp.push(Value::Null), } } @@ -312,6 +316,13 @@ impl VirtualMachine { self.pc = addr; } } + OpCode::JmpIfTrue => { + let addr = self.read_u32()? as usize; + let val = self.pop()?; + if let Value::Boolean(true) = val { + self.pc = addr; + } + } OpCode::PushConst => { let idx = self.read_u32()? as usize; let val = self.program.constant_pool.get(idx).cloned().ok_or("Invalid constant index")?; @@ -319,7 +330,11 @@ impl VirtualMachine { } OpCode::PushI64 => { let val = self.read_i64()?; - self.push(Value::Integer(val)); + self.push(Value::Int64(val)); + } + OpCode::PushI32 => { + let val = self.read_i32()?; + self.push(Value::Int32(val)); } OpCode::PushF64 => { let val = self.read_f64()?; @@ -343,40 +358,75 @@ impl VirtualMachine { self.push(b); } OpCode::Add => self.binary_op(|a, b| match (a, b) { - (Value::Integer(a), Value::Integer(b)) => Ok(Value::Integer(a.wrapping_add(b))), + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(b))), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(b))), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_add(b))), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_add(b as i64))), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)), - (Value::Integer(a), Value::Float(b)) => Ok(Value::Float(a as f64 + b)), - (Value::Float(a), Value::Integer(b)) => Ok(Value::Float(a + b as f64)), + (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(a as f64 + b)), + (Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a + b as f64)), + (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 + b)), + (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a + b as f64)), _ => Err("Invalid types for ADD".into()), })?, OpCode::Sub => self.binary_op(|a, b| match (a, b) { - (Value::Integer(a), Value::Integer(b)) => Ok(Value::Integer(a.wrapping_sub(b))), + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_sub(b))), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_sub(b))), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_sub(b))), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_sub(b as i64))), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)), - (Value::Integer(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)), - (Value::Float(a), Value::Integer(b)) => Ok(Value::Float(a - b as f64)), + (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)), + (Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a - b as f64)), + (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)), + (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)), _ => Err("Invalid types for SUB".into()), })?, OpCode::Mul => self.binary_op(|a, b| match (a, b) { - (Value::Integer(a), Value::Integer(b)) => Ok(Value::Integer(a.wrapping_mul(b))), + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_mul(b))), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_mul(b))), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_mul(b))), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_mul(b as i64))), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)), - (Value::Integer(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)), - (Value::Float(a), Value::Integer(b)) => Ok(Value::Float(a * b as f64)), + (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)), + (Value::Float(a), Value::Int32(b)) => Ok(Value::Float(a * b as f64)), + (Value::Int64(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)), + (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a * b as f64)), _ => Err("Invalid types for MUL".into()), })?, OpCode::Div => self.binary_op(|a, b| match (a, b) { - (Value::Integer(a), Value::Integer(b)) => { + (Value::Int32(a), Value::Int32(b)) => { if b == 0 { return Err("Division by zero".into()); } - Ok(Value::Integer(a / b)) + Ok(Value::Int32(a / b)) + } + (Value::Int64(a), Value::Int64(b)) => { + if b == 0 { return Err("Division by zero".into()); } + Ok(Value::Int64(a / b)) + } + (Value::Int32(a), Value::Int64(b)) => { + if b == 0 { return Err("Division by zero".into()); } + Ok(Value::Int64(a as i64 / b)) + } + (Value::Int64(a), Value::Int32(b)) => { + if b == 0 { return Err("Division by zero".into()); } + Ok(Value::Int64(a / b as i64)) } (Value::Float(a), Value::Float(b)) => { if b == 0.0 { return Err("Division by zero".into()); } Ok(Value::Float(a / b)) } - (Value::Integer(a), Value::Float(b)) => { + (Value::Int32(a), Value::Float(b)) => { if b == 0.0 { return Err("Division by zero".into()); } Ok(Value::Float(a as f64 / b)) } - (Value::Float(a), Value::Integer(b)) => { + (Value::Float(a), Value::Int32(b)) => { + if b == 0 { return Err("Division by zero".into()); } + Ok(Value::Float(a / b as f64)) + } + (Value::Int64(a), Value::Float(b)) => { + if b == 0.0 { return Err("Division by zero".into()); } + Ok(Value::Float(a as f64 / b)) + } + (Value::Float(a), Value::Int64(b)) => { if b == 0 { return Err("Division by zero".into()); } Ok(Value::Float(a / b as f64)) } @@ -385,22 +435,24 @@ impl VirtualMachine { OpCode::Eq => self.binary_op(|a, b| Ok(Value::Boolean(a == b)))?, OpCode::Neq => self.binary_op(|a, b| Ok(Value::Boolean(a != b)))?, OpCode::Lt => self.binary_op(|a, b| { - match (a, b) { - (Value::Integer(a), Value::Integer(b)) => Ok(Value::Boolean(a < b)), - (Value::Float(a), Value::Float(b)) => Ok(Value::Boolean(a < b)), - (Value::Integer(a), Value::Float(b)) => Ok(Value::Boolean((a as f64) < b)), - (Value::Float(a), Value::Integer(b)) => Ok(Value::Boolean(a < (b as f64))), - _ => Err("Invalid types for LT".into()), - } + a.partial_cmp(&b) + .map(|o| Value::Boolean(o == std::cmp::Ordering::Less)) + .ok_or_else(|| "Invalid types for LT".into()) })?, OpCode::Gt => self.binary_op(|a, b| { - match (a, b) { - (Value::Integer(a), Value::Integer(b)) => Ok(Value::Boolean(a > b)), - (Value::Float(a), Value::Float(b)) => Ok(Value::Boolean(a > b)), - (Value::Integer(a), Value::Float(b)) => Ok(Value::Boolean((a as f64) > b)), - (Value::Float(a), Value::Integer(b)) => Ok(Value::Boolean(a > (b as f64))), - _ => Err("Invalid types for GT".into()), - } + a.partial_cmp(&b) + .map(|o| Value::Boolean(o == std::cmp::Ordering::Greater)) + .ok_or_else(|| "Invalid types for GT".into()) + })?, + OpCode::Lte => self.binary_op(|a, b| { + a.partial_cmp(&b) + .map(|o| Value::Boolean(o != std::cmp::Ordering::Greater)) + .ok_or_else(|| "Invalid types for LTE".into()) + })?, + OpCode::Gte => self.binary_op(|a, b| { + a.partial_cmp(&b) + .map(|o| Value::Boolean(o != std::cmp::Ordering::Less)) + .ok_or_else(|| "Invalid types for GTE".into()) })?, OpCode::And => self.binary_op(|a, b| match (a, b) { (Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a && b)), @@ -418,6 +470,50 @@ impl VirtualMachine { return Err("Invalid type for NOT".into()); } } + OpCode::BitAnd => self.binary_op(|a, b| match (a, b) { + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a & b)), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a & b)), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) & b)), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a & (b as i64))), + _ => Err("Invalid types for BitAnd".into()), + })?, + OpCode::BitOr => self.binary_op(|a, b| match (a, b) { + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a | b)), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a | b)), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) | b)), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a | (b as i64))), + _ => Err("Invalid types for BitOr".into()), + })?, + OpCode::BitXor => self.binary_op(|a, b| match (a, b) { + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a ^ b)), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a ^ b)), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64) ^ b)), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a ^ (b as i64))), + _ => Err("Invalid types for BitXor".into()), + })?, + OpCode::Shl => self.binary_op(|a, b| match (a, b) { + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), + _ => Err("Invalid types for Shl".into()), + })?, + OpCode::Shr => self.binary_op(|a, b| match (a, b) { + (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))), + (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), + (Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))), + (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), + _ => Err("Invalid types for Shr".into()), + })?, + OpCode::Neg => { + let val = self.pop()?; + match val { + Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())), + Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())), + Value::Float(a) => self.push(Value::Float(-a)), + _ => return Err("Invalid type for Neg".into()), + } + } OpCode::GetGlobal => { let idx = self.read_u32()? as usize; let val = self.globals.get(idx).cloned().ok_or("Invalid global index")?; @@ -548,6 +644,20 @@ impl VirtualMachine { Ok(u32::from_le_bytes(bytes)) } + fn read_i32(&mut self) -> Result { + if self.pc + 4 > self.program.rom.len() { + return Err("Unexpected end of ROM".into()); + } + let bytes = [ + self.program.rom[self.pc], + self.program.rom[self.pc + 1], + self.program.rom[self.pc + 2], + self.program.rom[self.pc + 3], + ]; + self.pc += 4; + Ok(i32::from_le_bytes(bytes)) + } + fn read_i64(&mut self) -> Result { if self.pc + 8 > self.program.rom.len() { return Err("Unexpected end of ROM".into()); @@ -661,7 +771,7 @@ mod tests { let mut hw = MockHardware; vm.step(&mut native, &mut hw).unwrap(); - assert_eq!(vm.peek().unwrap(), &Value::Integer(42)); + assert_eq!(vm.peek().unwrap(), &Value::Int64(42)); } #[test] @@ -814,7 +924,7 @@ mod tests { vm2.step(&mut native, &mut hw).unwrap(); // RET assert_eq!(vm2.operand_stack.len(), 1); - assert_eq!(vm2.pop().unwrap(), Value::Integer(123)); + assert_eq!(vm2.pop().unwrap(), Value::Int64(123)); } #[test] @@ -866,16 +976,12 @@ mod tests { 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)); + assert_eq!(vm.operand_stack.last().unwrap(), &Value::Int64(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 + assert_eq!(vm.operand_stack.last().unwrap(), &Value::Int64(1)); } #[test] @@ -929,12 +1035,149 @@ mod tests { // 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)); + assert_eq!(vm.operand_stack[0], Value::Int64(100)); + assert_eq!(vm.operand_stack[1], Value::Int64(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. } + + #[test] + fn test_push_i32() { + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.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; + + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); + } + + #[test] + fn test_bitwise_promotion() { + let mut native = MockNative; + let mut hw = MockHardware; + + // i32 & i32 -> i32 + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&0xF0i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&0x0Fi32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::BitAnd as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.pop().unwrap(), Value::Int32(0)); + + // i32 | i64 -> i64 + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&0xF0i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes()); + rom.extend_from_slice(&0x0Fi64.to_le_bytes()); + rom.extend_from_slice(&(OpCode::BitOr as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.pop().unwrap(), Value::Int64(0xFF)); + } + + #[test] + fn test_comparisons_lte_gte() { + let mut native = MockNative; + let mut hw = MockHardware; + + // 10 <= 20 (true) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&10i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&20i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Lte as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.pop().unwrap(), Value::Boolean(true)); + + // 20 >= 20 (true) + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&20i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&20i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Gte as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.pop().unwrap(), Value::Boolean(true)); + } + + #[test] + fn test_negation() { + let mut native = MockNative; + let mut hw = MockHardware; + + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Neg as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.step(&mut native, &mut hw).unwrap(); + vm.step(&mut native, &mut hw).unwrap(); + assert_eq!(vm.pop().unwrap(), Value::Int32(-42)); + } + + #[test] + fn test_jmp_if_true() { + let mut native = MockNative; + let mut hw = MockHardware; + + // Corrected Calculations: + // 0-1: PushBool + // 2: 1 (u8) + // 3-4: JmpIfTrue + // 5-8: addr (u32) + // 9-10: Halt (Offset 9) + // 11-12: PushI32 (Offset 11) + // 13-16: 100 (i32) + // 17-18: Halt + + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); + rom.push(1); + rom.extend_from_slice(&(OpCode::JmpIfTrue as u16).to_le_bytes()); + rom.extend_from_slice(&(11u32).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); // Offset 9 + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); // Offset 11 + rom.extend_from_slice(&100i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.step(&mut native, &mut hw).unwrap(); // PushBool + vm.step(&mut native, &mut hw).unwrap(); // JmpIfTrue + assert_eq!(vm.pc, 11); + vm.step(&mut native, &mut hw).unwrap(); // PushI32 + assert_eq!(vm.pop().unwrap(), Value::Int32(100)); + } } diff --git a/docs/specs/topics/chapter-2.md b/docs/specs/topics/chapter-2.md index b1226cfc..614fba58 100644 --- a/docs/specs/topics/chapter-2.md +++ b/docs/specs/topics/chapter-2.md @@ -61,13 +61,21 @@ Properties: | Type | Description | | --------- | ------------------------- | -| `integer` | 64-bit signed integer | +| `int32` | 32-bit signed integer | +| `int64` | 64-bit signed integer | | `float` | 64-bit floating point | | `boolean` | true/false | | `string` | immutable UTF-8 | | `null` | absence of value | | `ref` | heap reference | +### 3.1 Numeric Promotion +The VM promotes types automatically during operations: +* `int32` + `int32` → `int32` +* `int32` + `int64` → `int64` +* `int` + `float` → `float` +* Bitwise operations promote `int32` to `int64` if any operand is `int64`. + Do not exist: * magic coercions @@ -123,6 +131,7 @@ State: | `HALT` | 1 | Terminates execution | | `JMP addr` | 2 | Unconditional jump | | `JMP_IF_FALSE addr` | 3 | Jumps if top is false | +| `JMP_IF_TRUE addr` | 3 | Jumps if top is true | --- @@ -134,6 +143,10 @@ State: | `POP` | 1 | Removes top | | `DUP` | 1 | Duplicates top | | `SWAP` | 1 | Swaps two tops | +| `PUSH_I32 v` | 2 | Pushes 32-bit int | +| `PUSH_I64 v` | 2 | Pushes 64-bit int | +| `PUSH_F64 v` | 2 | Pushes 64-bit float | +| `PUSH_BOOL v` | 2 | Pushes boolean | --- @@ -156,9 +169,17 @@ State: | `NEQ` | 2 | | `LT` | 2 | | `GT` | 2 | +| `LTE` | 2 | +| `GTE` | 2 | | `AND` | 2 | | `OR` | 2 | | `NOT` | 1 | +| `BIT_AND` | 2 | +| `BIT_OR` | 2 | +| `BIT_XOR` | 2 | +| `SHL` | 2 | +| `SHR` | 2 | +| `NEG` | 1 | --- -- 2.47.2 From 89bd82ab82489ba0954d0db98962ce3be7f2bca5 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 09:31:45 +0000 Subject: [PATCH 5/7] improve stack contract --- .../src/prometeu_os/prometeu_os.rs | 38 ++++++++++++++----- docs/specs/topics/chapter-2.md | 27 +++++++++++-- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index f216b10b..04748f08 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -290,7 +290,7 @@ impl PrometeuOS { // Helper para syscalls - fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result { + fn syscall_log_write(&mut self, vm: &mut VirtualMachine, level_val: i64, tag: u16, msg: String) -> Result { let level = match level_val { 0 => LogLevel::Trace, 1 => LogLevel::Debug, @@ -308,6 +308,7 @@ impl PrometeuOS { self.logs_written_this_frame.insert(app_id, count + 1); self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string()); } + vm.push(Value::Null); return Ok(50); } @@ -320,6 +321,7 @@ impl PrometeuOS { self.log(level, LogSource::App { app_id }, tag, final_msg); + vm.push(Value::Null); Ok(100) } @@ -529,6 +531,12 @@ mod tests { let recent = os.log_service.get_recent(1); assert_eq!(recent[0].msg, "Tagged Log"); assert_eq!(recent[0].tag, 42); + assert_eq!(vm.pop().unwrap(), Value::Null); + + // 6. GFX Syscall return test + vm.push(Value::Int64(1)); // color_idx + os.syscall(0x1001, &mut vm, &mut hw).unwrap(); // gfx.clear + assert_eq!(vm.pop().unwrap(), Value::Null); } } @@ -552,24 +560,27 @@ impl NativeInterface for PrometeuOS { // system.has_cart() -> bool 0x0001 => { // Returns true if a cartridge is available. + vm.push(Value::Boolean(true)); // For now, assume true or check state Ok(10) } - // system.run_cart() + // system.run_cart() -> null 0x0002 => { // Triggers loading and execution of the current cartridge. + vm.push(Value::Null); Ok(100) } // --- GFX Syscalls --- - // gfx.clear(color_index) + // gfx.clear(color_index) -> null 0x1001 => { let color_idx = vm.pop_integer()? as usize; let color = self.get_color(color_idx, hw); hw.gfx_mut().clear(color); + vm.push(Value::Null); Ok(100) } - // gfx.draw_rect(x, y, w, h, color_index) + // gfx.draw_rect(x, y, w, h, color_index) -> null 0x1002 => { let color_idx = vm.pop_integer()? as usize; let h = vm.pop_integer()? as i32; @@ -578,9 +589,10 @@ impl NativeInterface for PrometeuOS { let x = vm.pop_integer()? as i32; let color = self.get_color(color_idx, hw); hw.gfx_mut().fill_rect(x, y, w, h, color); + vm.push(Value::Null); Ok(200) } - // gfx.draw_line(x1, y1, x2, y2, color_index) + // gfx.draw_line(x1, y1, x2, y2, color_index) -> null 0x1003 => { let color_idx = vm.pop_integer()? as usize; let y2 = vm.pop_integer()? as i32; @@ -589,9 +601,10 @@ impl NativeInterface for PrometeuOS { let x1 = vm.pop_integer()? as i32; let color = self.get_color(color_idx, hw); hw.gfx_mut().draw_line(x1, y1, x2, y2, color); + vm.push(Value::Null); Ok(200) } - // gfx.draw_circle(x, y, r, color_index) + // gfx.draw_circle(x, y, r, color_index) -> null 0x1004 => { let color_idx = vm.pop_integer()? as usize; let r = vm.pop_integer()? as i32; @@ -599,9 +612,10 @@ impl NativeInterface for PrometeuOS { let x = vm.pop_integer()? as i32; let color = self.get_color(color_idx, hw); hw.gfx_mut().draw_circle(x, y, r, color); + vm.push(Value::Null); Ok(200) } - // gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) + // gfx.draw_disc(x, y, r, border_color_idx, fill_color_idx) -> null 0x1005 => { let fill_color_idx = vm.pop_integer()? as usize; let border_color_idx = vm.pop_integer()? as usize; @@ -611,9 +625,10 @@ impl NativeInterface for PrometeuOS { let fill_color = self.get_color(fill_color_idx, hw); let border_color = self.get_color(border_color_idx, hw); hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color); + vm.push(Value::Null); Ok(300) } - // gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) + // gfx.draw_square(x, y, w, h, border_color_idx, fill_color_idx) -> null 0x1006 => { let fill_color_idx = vm.pop_integer()? as usize; let border_color_idx = vm.pop_integer()? as usize; @@ -624,6 +639,7 @@ impl NativeInterface for PrometeuOS { let fill_color = self.get_color(fill_color_idx, hw); let border_color = self.get_color(border_color_idx, hw); hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); + vm.push(Value::Null); Ok(200) } @@ -657,6 +673,7 @@ impl NativeInterface for PrometeuOS { if let Some(s) = sample { hw.audio_mut().play(s, voice_id, volume, pan, pitch, 0, crate::hardware::LoopMode::Off); } + vm.push(Value::Null); Ok(300) } @@ -718,6 +735,7 @@ impl NativeInterface for PrometeuOS { 0x4004 => { let handle = vm.pop_integer()? as u32; self.open_files.remove(&handle); + vm.push(Value::Null); Ok(100) } // FS_LISTDIR(path) @@ -770,7 +788,7 @@ impl NativeInterface for PrometeuOS { _ => return Err("Expected string message".into()), }; let level = vm.pop_integer()?; - self.syscall_log_write(level, 0, msg) + self.syscall_log_write(vm, level, 0, msg) } // LOG_WRITE_TAG(level, tag, msg) 0x5002 => { @@ -780,7 +798,7 @@ impl NativeInterface for PrometeuOS { }; let tag = vm.pop_integer()? as u16; let level = vm.pop_integer()?; - self.syscall_log_write(level, tag, msg) + self.syscall_log_write(vm, level, tag, msg) } _ => Err(format!("Unknown syscall: 0x{:08X}", id)), diff --git a/docs/specs/topics/chapter-2.md b/docs/specs/topics/chapter-2.md index 614fba58..daa4d3f6 100644 --- a/docs/specs/topics/chapter-2.md +++ b/docs/specs/topics/chapter-2.md @@ -84,11 +84,30 @@ Do not exist: --- -## 4. Stack Conventions +## 4. Stack Conventions & Calling ABI -* Operations use the top of the stack -* Results always return to the stack -* Last pushed = first consumed +* Operations use the top of the stack. +* Results always return to the stack. +* **LIFO Order:** Last pushed = first consumed. +* **Mandatory Return:** Every function (`Call`) and `Syscall` MUST leave exactly **one** value on the stack upon completion. If there is no meaningful value to return, `Null` must be pushed. + +### 4.1 Calling Convention (Call / Ret) + +1. **Arguments:** The caller pushes arguments in order (arg0, arg1, ..., argN-1). +2. **Execution:** The `Call` instruction specifies `args_count`. These `N` values become the **locals** of the new frame (local 0 = arg0, local 1 = arg1, etc.). +3. **Return Value:** Before executing `Ret`, the callee MUST push its return value. +4. **Cleanup:** The `Ret` instruction is responsible for: + - Popping the return value. + - Removing all locals (the arguments) from the operand stack. + - Re-pushing the return value. + - Restoring the previous frame and PC. + +### 4.2 Syscall Convention + +1. **Arguments:** The caller pushes arguments in order. +2. **Execution:** The native implementation pops arguments as needed. Since it's a stack, it will pop them in reverse order (argN-1 first, then argN-2, ..., arg0). +3. **Return Value:** The native implementation MUST push exactly one value onto the stack before returning to the VM. +4. **Cleanup:** The native implementation is responsible for popping all arguments it expects. Example: -- 2.47.2 From 481deacc6528e876e45e13c0ed58a006a7354ef6 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 09:51:31 +0000 Subject: [PATCH 6/7] add trap and dropN --- .../src/virtual_machine/opcode.rs | 8 +++ .../src/virtual_machine/virtual_machine.rs | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/crates/prometeu-core/src/virtual_machine/opcode.rs b/crates/prometeu-core/src/virtual_machine/opcode.rs index f71168a1..11b4fb9b 100644 --- a/crates/prometeu-core/src/virtual_machine/opcode.rs +++ b/crates/prometeu-core/src/virtual_machine/opcode.rs @@ -8,6 +8,7 @@ pub enum OpCode { Jmp = 0x02, JmpIfFalse = 0x03, JmpIfTrue = 0x04, + Trap = 0x05, // 6.2 Stack PushConst = 0x10, @@ -18,6 +19,7 @@ pub enum OpCode { PushF64 = 0x15, PushBool = 0x16, PushI32 = 0x17, + PopN = 0x18, // 6.3 Arithmetic Add = 0x20, @@ -74,6 +76,7 @@ impl TryFrom for OpCode { 0x02 => Ok(OpCode::Jmp), 0x03 => Ok(OpCode::JmpIfFalse), 0x04 => Ok(OpCode::JmpIfTrue), + 0x05 => Ok(OpCode::Trap), 0x10 => Ok(OpCode::PushConst), 0x11 => Ok(OpCode::Pop), 0x12 => Ok(OpCode::Dup), @@ -82,6 +85,7 @@ impl TryFrom for OpCode { 0x15 => Ok(OpCode::PushF64), 0x16 => Ok(OpCode::PushBool), 0x17 => Ok(OpCode::PushI32), + 0x18 => Ok(OpCode::PopN), 0x20 => Ok(OpCode::Add), 0x21 => Ok(OpCode::Sub), 0x22 => Ok(OpCode::Mul), @@ -127,8 +131,10 @@ impl OpCode { OpCode::Jmp => 2, OpCode::JmpIfFalse => 3, OpCode::JmpIfTrue => 3, + OpCode::Trap => 1, OpCode::PushConst => 2, OpCode::Pop => 1, + OpCode::PopN => 2, OpCode::Dup => 1, OpCode::Swap => 1, OpCode::PushI64 => 2, @@ -178,7 +184,9 @@ mod tests { #[test] fn test_opcode_decoding() { assert_eq!(OpCode::try_from(0x00).unwrap(), OpCode::Nop); + assert_eq!(OpCode::try_from(0x05).unwrap(), OpCode::Trap); assert_eq!(OpCode::try_from(0x10).unwrap(), OpCode::PushConst); + assert_eq!(OpCode::try_from(0x18).unwrap(), OpCode::PopN); assert_eq!(OpCode::try_from(0x20).unwrap(), OpCode::Add); assert_eq!(OpCode::try_from(0x70).unwrap(), OpCode::Syscall); assert_eq!(OpCode::try_from(0x80).unwrap(), OpCode::FrameSync); diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index a6e69de3..4d336434 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -243,6 +243,14 @@ impl VirtualMachine { break; } + if opcode == OpCode::Trap { + self.pc += 2; // Advance PC past the opcode + self.cycles += OpCode::Trap.cycles(); + steps_executed += 1; + ending_reason = Some(LogicalFrameEndingReason::Breakpoint); + break; + } + // Execute a single step (Fetch-Decode-Execute) self.step(native, hw)?; steps_executed += 1; @@ -323,6 +331,10 @@ impl VirtualMachine { self.pc = addr; } } + OpCode::Trap => { + // Handled in run_budget for interruption, + // but we need to advance PC if executed via step() directly. + } OpCode::PushConst => { let idx = self.read_u32()? as usize; let val = self.program.constant_pool.get(idx).cloned().ok_or("Invalid constant index")?; @@ -347,6 +359,12 @@ impl VirtualMachine { OpCode::Pop => { self.pop()?; } + OpCode::PopN => { + let n = self.read_u16()?; + for _ in 0..n { + self.pop()?; + } + } OpCode::Dup => { let val = self.peek()?.clone(); self.push(val); @@ -1180,4 +1198,46 @@ mod tests { vm.step(&mut native, &mut hw).unwrap(); // PushI32 assert_eq!(vm.pop().unwrap(), Value::Int32(100)); } + + #[test] + fn test_trap_opcode() { + let mut native = MockNative; + let mut hw = MockHardware; + + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&42i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Trap as u16).to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + let report = vm.run_budget(100, &mut native, &mut hw).unwrap(); + + assert_eq!(report.reason, LogicalFrameEndingReason::Breakpoint); + assert_eq!(vm.pc, 8); // PushI32 (6 bytes) + Trap (2 bytes) + assert_eq!(vm.peek().unwrap(), &Value::Int32(42)); + } + + #[test] + fn test_pop_n_opcode() { + let mut native = MockNative; + let mut hw = MockHardware; + + let mut rom = Vec::new(); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&1i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&2i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes()); + rom.extend_from_slice(&3i32.to_le_bytes()); + rom.extend_from_slice(&(OpCode::PopN as u16).to_le_bytes()); + rom.extend_from_slice(&2u16.to_le_bytes()); + rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let mut vm = VirtualMachine::new(rom, vec![]); + vm.run_budget(100, &mut native, &mut hw).unwrap(); + + assert_eq!(vm.pop().unwrap(), Value::Int32(1)); + assert!(vm.pop().is_err()); // Stack should be empty + } } -- 2.47.2 From 84d57897b1ce91039dfd35ac98ee0d420089add2 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Tue, 20 Jan 2026 10:17:58 +0000 Subject: [PATCH 7/7] create prometeu-bytecode --- Cargo.lock | 5 + crates/prometeu-bytecode/Cargo.toml | 7 + crates/prometeu-bytecode/README.md | 152 ++++++++++++++++++ crates/prometeu-bytecode/src/abi.rs | 29 ++++ crates/prometeu-bytecode/src/asm.rs | 69 ++++++++ crates/prometeu-bytecode/src/disasm.rs | 64 ++++++++ crates/prometeu-bytecode/src/lib.rs | 51 ++++++ .../src}/opcode.rs | 29 +--- crates/prometeu-bytecode/src/pbc.rs | 111 +++++++++++++ crates/prometeu-bytecode/src/readwrite.rs | 41 +++++ crates/prometeu-core/Cargo.toml | 3 +- .../prometeu-core/src/virtual_machine/mod.rs | 3 +- .../src/virtual_machine/virtual_machine.rs | 90 ++--------- crates/prometeuc/Cargo.toml | 7 + crates/prometeuc/src/codegen/mod.rs | 2 + crates/prometeuc/src/main.rs | 5 + 16 files changed, 559 insertions(+), 109 deletions(-) create mode 100644 crates/prometeu-bytecode/Cargo.toml create mode 100644 crates/prometeu-bytecode/README.md create mode 100644 crates/prometeu-bytecode/src/abi.rs create mode 100644 crates/prometeu-bytecode/src/asm.rs create mode 100644 crates/prometeu-bytecode/src/disasm.rs create mode 100644 crates/prometeu-bytecode/src/lib.rs rename crates/{prometeu-core/src/virtual_machine => prometeu-bytecode/src}/opcode.rs (82%) create mode 100644 crates/prometeu-bytecode/src/pbc.rs create mode 100644 crates/prometeu-bytecode/src/readwrite.rs create mode 100644 crates/prometeuc/Cargo.toml create mode 100644 crates/prometeuc/src/codegen/mod.rs create mode 100644 crates/prometeuc/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 367bed91..552dd83b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1582,10 +1582,15 @@ dependencies = [ "winit", ] +[[package]] +name = "prometeu-bytecode" +version = "0.1.0" + [[package]] name = "prometeu-core" version = "0.1.0" dependencies = [ + "prometeu-bytecode", "serde", "serde_json", ] diff --git a/crates/prometeu-bytecode/Cargo.toml b/crates/prometeu-bytecode/Cargo.toml new file mode 100644 index 00000000..6d00aa1b --- /dev/null +++ b/crates/prometeu-bytecode/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "prometeu-bytecode" +version = "0.1.0" +edition = "2021" + +[dependencies] +# No dependencies for now diff --git a/crates/prometeu-bytecode/README.md b/crates/prometeu-bytecode/README.md new file mode 100644 index 00000000..cc96b1b1 --- /dev/null +++ b/crates/prometeu-bytecode/README.md @@ -0,0 +1,152 @@ +# prometeu-bytecode + +Contrato oficial (ABI) do ecossistema PROMETEU. Este crate define o conjunto de instruções (ISA), o formato de arquivo `.pbc` (Prometeu ByteCode) e ferramentas básicas de montagem (Assembler) e desmontagem (Disassembler). + +## Design + +A PVM (Prometeu Virtual Machine) é uma máquina baseada em pilha (**stack-based**). A maioria das instruções opera nos valores do topo da pilha de operandos. O formato de dados padrão para multi-byte na ROM é **Little-Endian**. + +### Convenção de Notação da Pilha +Nas tabelas abaixo, usamos a seguinte notação para representar o estado da pilha: +`[a, b] -> [c]` +Significa que a instrução remove `a` e `b` da pilha (onde `b` estava no topo) e insere `c` no topo. + +--- + +## Conjunto de Instruções (ISA) + +### 6.1 Controle de Execução + +| OpCode | Valor | Operandos | Pilha | Descrição | +| :--- | :--- | :--- | :--- | :--- | +| `Nop` | `0x00` | - | `[] -> []` | Nenhuma operação. | +| `Halt` | `0x01` | - | `[] -> []` | Interrompe a execução da VM permanentemente. | +| `Jmp` | `0x02` | `addr: u32` | `[] -> []` | Salto incondicional para o endereço absoluto `addr`. | +| `JmpIfFalse`| `0x03` | `addr: u32` | `[bool] -> []` | Salta para `addr` se o valor desempilhado for `false`. | +| `JmpIfTrue` | `0x04` | `addr: u32` | `[bool] -> []` | Salta para `addr` se o valor desempilhado for `true`. | +| `Trap` | `0x05` | - | `[] -> []` | Interrupção para debugger (breakpoint). | + +### 6.2 Manipulação da Pilha + +| OpCode | Valor | Operandos | Pilha | Descrição | +| :--- | :--- | :--- | :--- | :--- | +| `PushConst` | `0x10` | `idx: u32` | `[] -> [val]` | Carrega a constante do índice `idx` da Constant Pool. | +| `Pop` | `0x11` | - | `[val] -> []` | Remove e descarta o valor do topo da pilha. | +| `Dup` | `0x12` | - | `[val] -> [val, val]` | Duplica o valor no topo da pilha. | +| `Swap` | `0x13` | - | `[a, b] -> [b, a]` | Inverte a posição dos dois valores no topo. | +| `PushI64` | `0x14` | `val: i64` | `[] -> [i64]` | Empilha um inteiro de 64 bits imediato. | +| `PushF64` | `0x15` | `val: f64` | `[] -> [f64]` | Empilha um ponto flutuante de 64 bits imediato. | +| `PushBool` | `0x16` | `val: u8` | `[] -> [bool]` | Empilha um booleano (0=false, 1=true). | +| `PushI32` | `0x17` | `val: i32` | `[] -> [i32]` | Empilha um inteiro de 32 bits imediato. | +| `PopN` | `0x18` | `n: u16` | `[...] -> [...]` | Remove `n` valores da pilha de uma vez. | + +### 6.3 Aritmética +A VM realiza promoção automática de tipos (ex: `i32` + `f64` resulta em `f64`). + +| OpCode | Valor | Pilha | Descrição | +| :--- | :--- | :--- | :--- | +| `Add` | `0x20` | `[a, b] -> [a + b]` | Soma os dois valores do topo. | +| `Sub` | `0x21` | `[a, b] -> [a - b]` | Subtrai `b` de `a`. | +| `Mul` | `0x22` | `[a, b] -> [a * b]` | Multiplica os dois valores do topo. | +| `Div` | `0x23` | `[a, b] -> [a / b]` | Divide `a` por `b`. Erro se `b == 0`. | +| `Neg` | `0x3E` | `[a] -> [-a]` | Inverte o sinal numérico. | + +### 6.4 Lógica e Comparação + +| OpCode | Valor | Pilha | Descrição | +| :--- | :--- | :--- | :--- | +| `Eq` | `0x30` | `[a, b] -> [bool]` | Testa igualdade. | +| `Neq` | `0x31` | `[a, b] -> [bool]` | Testa desigualdade. | +| `Lt` | `0x32` | `[a, b] -> [bool]` | `a < b` | +| `Gt` | `0x33` | `[a, b] -> [bool]` | `a > b` | +| `Lte` | `0x3C` | `[a, b] -> [bool]` | `a <= b` | +| `Gte` | `0x3D` | `[a, b] -> [bool]` | `a >= b` | +| `And` | `0x34` | `[a, b] -> [bool]` | AND lógico (booleano). | +| `Or` | `0x35` | `[a, b] -> [bool]` | OR lógico (booleano). | +| `Not` | `0x36` | `[a] -> [!a]` | NOT lógico. | +| `BitAnd` | `0x37` | `[a, b] -> [int]` | AND bit a bit. | +| `BitOr` | `0x38` | `[a, b] -> [int]` | OR bit a bit. | +| `BitXor` | `0x39` | `[a, b] -> [int]` | XOR bit a bit. | +| `Shl` | `0x3A` | `[a, b] -> [int]` | Shift Left: `a << b`. | +| `Shr` | `0x3B` | `[a, b] -> [int]` | Shift Right: `a >> b`. | + +### 6.5 Variáveis e Memória + +| OpCode | Valor | Operandos | Pilha | Descrição | +| :--- | :--- | :--- | :--- | :--- | +| `GetGlobal`| `0x40` | `idx: u32` | `[] -> [val]` | Carrega valor da global `idx`. | +| `SetGlobal`| `0x41` | `idx: u32` | `[val] -> []` | Armazena topo na global `idx`. | +| `GetLocal` | `0x42` | `idx: u32` | `[] -> [val]` | Carrega local `idx` do frame atual. | +| `SetLocal` | `0x43` | `idx: u32` | `[val] -> []` | Armazena topo na local `idx` do frame atual. | + +### 6.6 Funções e Escopo + +| OpCode | Valor | Operandos | Pilha | Descrição | +| :--- | :--- | :--- | :--- | :--- | +| `Call` | `0x50` | `addr: u32, args: u32` | `[a1, a2] -> [...]` | Chama `addr`. Os `args` valores no topo viram locais do novo frame. | +| `Ret` | `0x51` | - | `[val] -> [val]` | Retorna da função atual, limpando o frame e devolvendo o valor do topo. | +| `PushScope`| `0x52` | - | `[] -> []` | Inicia um sub-escopo (bloco) para locais temporários. | +| `PopScope` | `0x53` | - | `[] -> []` | Finaliza sub-escopo, removendo locais criados nele da pilha. | + +### 6.7 Heap (Memória Dinâmica) + +| OpCode | Valor | Operandos | Pilha | Descrição | +| :--- | :--- | :--- | :--- | :--- | +| `Alloc` | `0x60` | `size: u32` | `[] -> [ref]` | Aloca `size` slots no heap e retorna uma referência. | +| `LoadRef` | `0x61` | `offset: u32`| `[ref] -> [val]` | Lê valor do heap no endereço `ref + offset`. | +| `StoreRef`| `0x62` | `offset: u32`| `[ref, val] -> []` | Escreve `val` no heap no endereço `ref + offset`. | + +### 6.8 Sistema e Sincronização + +| OpCode | Valor | Operandos | Pilha | Descrição | +| :--- | :--- | :--- | :--- | :--- | +| `Syscall` | `0x70` | `id: u32` | `[...] -> [...]` | Invoca uma função do sistema/firmware. A pilha depende da syscall. | +| `FrameSync`| `0x80` | - | `[] -> []` | Marca o fim do processamento do frame lógico atual (60 FPS). | + +--- + +## Estrutura do PBC (Prometeu ByteCode) + +O PBC é o formato binário oficial para programas Prometeu. + +```rust +// Exemplo de como carregar um arquivo PBC +let bytes = std::fs::read("game.pbc")?; +let pbc = prometeu_bytecode::pbc::parse_pbc(&bytes)?; + +println!("ROM size: {} bytes", pbc.rom.len()); +println!("Constants: {}", pbc.cp.len()); +``` + +--- + +## Assembler e Disassembler + +Este crate fornece ferramentas para facilitar a geração e inspeção de código. + +### Montagem (Assembler) +```rust +use prometeu_bytecode::asm::{assemble, Asm, Operand}; +use prometeu_bytecode::opcode::OpCode; + +let instructions = vec![ + Asm::Op(OpCode::PushI32, vec![Operand::I32(10)]), + Asm::Op(OpCode::PushI32, vec![Operand::I32(20)]), + Asm::Op(OpCode::Add, vec![]), + Asm::Op(OpCode::Halt, vec![]), +]; + +let rom_bytes = assemble(&instructions).unwrap(); +``` + +### Desmontagem (Disassembler) +```rust +use prometeu_bytecode::disasm::disasm; + +let rom = vec![/* ... bytes ... */]; +let code = disasm(&rom); + +for instr in code { + println!("{:04X}: {:?} {:?}", instr.pc, instr.opcode, instr.operands); +} +``` diff --git a/crates/prometeu-bytecode/src/abi.rs b/crates/prometeu-bytecode/src/abi.rs new file mode 100644 index 00000000..3cde16d2 --- /dev/null +++ b/crates/prometeu-bytecode/src/abi.rs @@ -0,0 +1,29 @@ +use crate::opcode::OpCode; + +pub fn operand_size(opcode: OpCode) -> usize { + match opcode { + OpCode::PushConst => 4, + OpCode::PushI32 => 4, + OpCode::PushI64 => 8, + OpCode::PushF64 => 8, + OpCode::PushBool => 1, + OpCode::PopN => 4, + OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => 4, + OpCode::GetGlobal | OpCode::SetGlobal => 4, + OpCode::GetLocal | OpCode::SetLocal => 4, + OpCode::Call => 8, // addr(u32) + args_count(u32) + OpCode::Syscall => 4, + _ => 0, + } +} + +pub fn is_jump(opcode: OpCode) -> bool { + match opcode { + OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => true, + _ => false, + } +} + +pub fn has_immediate(opcode: OpCode) -> bool { + operand_size(opcode) > 0 +} diff --git a/crates/prometeu-bytecode/src/asm.rs b/crates/prometeu-bytecode/src/asm.rs new file mode 100644 index 00000000..8079997f --- /dev/null +++ b/crates/prometeu-bytecode/src/asm.rs @@ -0,0 +1,69 @@ +use crate::opcode::OpCode; +use crate::readwrite::*; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub enum Operand { + U32(u32), + I32(i32), + I64(i64), + F64(f64), + Bool(bool), + Label(String), +} + +#[derive(Debug, Clone)] +pub enum Asm { + Op(OpCode, Vec), + Label(String), +} + +pub fn assemble(instructions: &[Asm]) -> Result, String> { + let mut labels = HashMap::new(); + let mut current_pc = 0u32; + + // First pass: resolve labels + for instr in instructions { + match instr { + Asm::Label(name) => { + labels.insert(name.clone(), current_pc); + } + Asm::Op(_opcode, operands) => { + current_pc += 2; // OpCode is u16 (2 bytes) + for operand in operands { + match operand { + Operand::U32(_) | Operand::I32(_) | Operand::Label(_) => current_pc += 4, + Operand::I64(_) | Operand::F64(_) => current_pc += 8, + Operand::Bool(_) => current_pc += 1, + } + } + } + } + } + + // Second pass: generate bytes + let mut rom = Vec::new(); + for instr in instructions { + match instr { + Asm::Label(_) => {} + Asm::Op(opcode, operands) => { + write_u16_le(&mut rom, *opcode as u16).map_err(|e| e.to_string())?; + for operand in operands { + match operand { + Operand::U32(v) => write_u32_le(&mut rom, *v).map_err(|e| e.to_string())?, + Operand::I32(v) => write_u32_le(&mut rom, *v as u32).map_err(|e| e.to_string())?, + Operand::I64(v) => write_i64_le(&mut rom, *v).map_err(|e| e.to_string())?, + Operand::F64(v) => write_f64_le(&mut rom, *v).map_err(|e| e.to_string())?, + Operand::Bool(v) => rom.push(if *v { 1 } else { 0 }), + Operand::Label(name) => { + let addr = labels.get(name).ok_or(format!("Undefined label: {}", name))?; + write_u32_le(&mut rom, *addr).map_err(|e| e.to_string())?; + } + } + } + } + } + } + + Ok(rom) +} diff --git a/crates/prometeu-bytecode/src/disasm.rs b/crates/prometeu-bytecode/src/disasm.rs new file mode 100644 index 00000000..c5e8376c --- /dev/null +++ b/crates/prometeu-bytecode/src/disasm.rs @@ -0,0 +1,64 @@ +use crate::opcode::OpCode; +use crate::readwrite::*; +use std::io::{Cursor, Read}; + +#[derive(Debug, Clone)] +pub struct Instr { + pub pc: u32, + pub opcode: OpCode, + pub operands: Vec, +} + +#[derive(Debug, Clone)] +pub enum DisasmOperand { + U32(u32), + I32(i32), + I64(i64), + F64(f64), + Bool(bool), +} + +pub fn disasm(rom: &[u8]) -> Result, String> { + let mut instructions = Vec::new(); + let mut cursor = Cursor::new(rom); + + while (cursor.position() as usize) < rom.len() { + let pc = cursor.position() as u32; + let op_val = read_u16_le(&mut cursor).map_err(|e| e.to_string())?; + let opcode = OpCode::try_from(op_val)?; + let mut operands = Vec::new(); + + match opcode { + OpCode::PushConst | OpCode::PushI32 | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue + | OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal + | OpCode::PopN | OpCode::Syscall => { + let v = read_u32_le(&mut cursor).map_err(|e| e.to_string())?; + operands.push(DisasmOperand::U32(v)); + } + OpCode::PushI64 => { + let v = read_i64_le(&mut cursor).map_err(|e| e.to_string())?; + operands.push(DisasmOperand::I64(v)); + } + OpCode::PushF64 => { + let v = read_f64_le(&mut cursor).map_err(|e| e.to_string())?; + operands.push(DisasmOperand::F64(v)); + } + OpCode::PushBool => { + let mut b_buf = [0u8; 1]; + cursor.read_exact(&mut b_buf).map_err(|e| e.to_string())?; + operands.push(DisasmOperand::Bool(b_buf[0] != 0)); + } + OpCode::Call => { + let addr = read_u32_le(&mut cursor).map_err(|e| e.to_string())?; + let args = read_u32_le(&mut cursor).map_err(|e| e.to_string())?; + operands.push(DisasmOperand::U32(addr)); + operands.push(DisasmOperand::U32(args)); + } + _ => {} + } + + instructions.push(Instr { pc, opcode, operands }); + } + + Ok(instructions) +} diff --git a/crates/prometeu-bytecode/src/lib.rs b/crates/prometeu-bytecode/src/lib.rs new file mode 100644 index 00000000..7050a9d6 --- /dev/null +++ b/crates/prometeu-bytecode/src/lib.rs @@ -0,0 +1,51 @@ +pub mod opcode; +pub mod abi; +pub mod pbc; +pub mod readwrite; +pub mod asm; +pub mod disasm; + +#[cfg(test)] +mod tests { + use crate::opcode::OpCode; + use crate::asm::{self, Asm, Operand}; + use crate::pbc::{self, PbcFile, ConstantPoolEntry}; + use crate::disasm; + + #[test] + fn test_golden_abi_roundtrip() { + // 1. Create a simple assembly program: PushI32 42; Halt + let instructions = vec![ + Asm::Op(OpCode::PushI32, vec![Operand::I32(42)]), + Asm::Op(OpCode::Halt, vec![]), + ]; + + let rom = asm::assemble(&instructions).expect("Failed to assemble"); + + // 2. Create a PBC file + let pbc_file = PbcFile { + cp: vec![ConstantPoolEntry::Int32(100)], // Random CP entry + rom, + }; + + let bytes = pbc::write_pbc(&pbc_file).expect("Failed to write PBC"); + + // 3. Parse it back + let parsed_pbc = pbc::parse_pbc(&bytes).expect("Failed to parse PBC"); + + assert_eq!(parsed_pbc.cp, pbc_file.cp); + assert_eq!(parsed_pbc.rom, pbc_file.rom); + + // 4. Disassemble ROM + let dis_instrs = disasm::disasm(&parsed_pbc.rom).expect("Failed to disassemble"); + + assert_eq!(dis_instrs.len(), 2); + assert_eq!(dis_instrs[0].opcode, OpCode::PushI32); + if let disasm::DisasmOperand::U32(v) = dis_instrs[0].operands[0] { + assert_eq!(v, 42); + } else { + panic!("Wrong operand type"); + } + assert_eq!(dis_instrs[1].opcode, OpCode::Halt); + } +} diff --git a/crates/prometeu-core/src/virtual_machine/opcode.rs b/crates/prometeu-bytecode/src/opcode.rs similarity index 82% rename from crates/prometeu-core/src/virtual_machine/opcode.rs rename to crates/prometeu-bytecode/src/opcode.rs index 11b4fb9b..c0e54dc4 100644 --- a/crates/prometeu-core/src/virtual_machine/opcode.rs +++ b/crates/prometeu-bytecode/src/opcode.rs @@ -1,4 +1,3 @@ - #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum OpCode { @@ -171,34 +170,8 @@ impl OpCode { OpCode::Alloc => 10, OpCode::LoadRef => 3, OpCode::StoreRef => 3, - OpCode::Syscall => 1, // Variable, but we'll use 1 as base or define via ID + OpCode::Syscall => 1, OpCode::FrameSync => 1, } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_opcode_decoding() { - assert_eq!(OpCode::try_from(0x00).unwrap(), OpCode::Nop); - assert_eq!(OpCode::try_from(0x05).unwrap(), OpCode::Trap); - assert_eq!(OpCode::try_from(0x10).unwrap(), OpCode::PushConst); - assert_eq!(OpCode::try_from(0x18).unwrap(), OpCode::PopN); - assert_eq!(OpCode::try_from(0x20).unwrap(), OpCode::Add); - assert_eq!(OpCode::try_from(0x70).unwrap(), OpCode::Syscall); - assert_eq!(OpCode::try_from(0x80).unwrap(), OpCode::FrameSync); - assert!(OpCode::try_from(0xFFFF).is_err()); - } - - #[test] - fn test_opcode_cycles() { - assert_eq!(OpCode::Nop.cycles(), 1); - assert_eq!(OpCode::Add.cycles(), 2); - assert_eq!(OpCode::Mul.cycles(), 4); - assert_eq!(OpCode::Div.cycles(), 6); - assert_eq!(OpCode::Alloc.cycles(), 10); - } -} \ No newline at end of file diff --git a/crates/prometeu-bytecode/src/pbc.rs b/crates/prometeu-bytecode/src/pbc.rs new file mode 100644 index 00000000..b69ca398 --- /dev/null +++ b/crates/prometeu-bytecode/src/pbc.rs @@ -0,0 +1,111 @@ +use crate::readwrite::*; +use std::io::{Cursor, Read, Write}; + +#[derive(Debug, Clone, PartialEq)] +pub enum ConstantPoolEntry { + Null, + Int64(i64), + Float64(f64), + Boolean(bool), + String(String), + Int32(i32), +} + +#[derive(Debug, Clone, Default)] +pub struct PbcFile { + pub cp: Vec, + pub rom: Vec, +} + +pub fn parse_pbc(bytes: &[u8]) -> Result { + if bytes.len() < 4 || &bytes[0..4] != b"PPBC" { + return Err("Invalid PBC signature".into()); + } + + let mut cursor = Cursor::new(&bytes[4..]); + + let cp_count = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize; + let mut cp = Vec::with_capacity(cp_count); + + for _ in 0..cp_count { + let mut tag_buf = [0u8; 1]; + cursor.read_exact(&mut tag_buf).map_err(|e| e.to_string())?; + let tag = tag_buf[0]; + + match tag { + 1 => { + let val = read_i64_le(&mut cursor).map_err(|e| e.to_string())?; + cp.push(ConstantPoolEntry::Int64(val)); + } + 2 => { + let val = read_f64_le(&mut cursor).map_err(|e| e.to_string())?; + cp.push(ConstantPoolEntry::Float64(val)); + } + 3 => { + let mut bool_buf = [0u8; 1]; + cursor.read_exact(&mut bool_buf).map_err(|e| e.to_string())?; + cp.push(ConstantPoolEntry::Boolean(bool_buf[0] != 0)); + } + 4 => { + let len = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize; + let mut s_buf = vec![0u8; len]; + cursor.read_exact(&mut s_buf).map_err(|e| e.to_string())?; + let s = String::from_utf8_lossy(&s_buf).into_owned(); + cp.push(ConstantPoolEntry::String(s)); + } + 5 => { + let val = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as i32; + cp.push(ConstantPoolEntry::Int32(val)); + } + _ => cp.push(ConstantPoolEntry::Null), + } + } + + let rom_size = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize; + let mut rom = vec![0u8; rom_size]; + cursor.read_exact(&mut rom).map_err(|e| e.to_string())?; + + Ok(PbcFile { cp, rom }) +} + +pub fn write_pbc(pbc: &PbcFile) -> Result, String> { + let mut out = Vec::new(); + out.write_all(b"PPBC").map_err(|e| e.to_string())?; + + write_u32_le(&mut out, pbc.cp.len() as u32).map_err(|e| e.to_string())?; + + for entry in &pbc.cp { + match entry { + ConstantPoolEntry::Null => { + out.write_all(&[0]).map_err(|e| e.to_string())?; + } + ConstantPoolEntry::Int64(v) => { + out.write_all(&[1]).map_err(|e| e.to_string())?; + write_i64_le(&mut out, *v).map_err(|e| e.to_string())?; + } + ConstantPoolEntry::Float64(v) => { + out.write_all(&[2]).map_err(|e| e.to_string())?; + write_f64_le(&mut out, *v).map_err(|e| e.to_string())?; + } + ConstantPoolEntry::Boolean(v) => { + out.write_all(&[3]).map_err(|e| e.to_string())?; + out.write_all(&[if *v { 1 } else { 0 }]).map_err(|e| e.to_string())?; + } + ConstantPoolEntry::String(v) => { + out.write_all(&[4]).map_err(|e| e.to_string())?; + let bytes = v.as_bytes(); + write_u32_le(&mut out, bytes.len() as u32).map_err(|e| e.to_string())?; + out.write_all(bytes).map_err(|e| e.to_string())?; + } + ConstantPoolEntry::Int32(v) => { + out.write_all(&[5]).map_err(|e| e.to_string())?; + write_u32_le(&mut out, *v as u32).map_err(|e| e.to_string())?; + } + } + } + + write_u32_le(&mut out, pbc.rom.len() as u32).map_err(|e| e.to_string())?; + out.write_all(&pbc.rom).map_err(|e| e.to_string())?; + + Ok(out) +} diff --git a/crates/prometeu-bytecode/src/readwrite.rs b/crates/prometeu-bytecode/src/readwrite.rs new file mode 100644 index 00000000..c52bc1a6 --- /dev/null +++ b/crates/prometeu-bytecode/src/readwrite.rs @@ -0,0 +1,41 @@ +use std::io::{self, Read, Write}; + +pub fn read_u16_le(mut reader: R) -> io::Result { + let mut buf = [0u8; 2]; + reader.read_exact(&mut buf)?; + Ok(u16::from_le_bytes(buf)) +} + +pub fn read_u32_le(mut reader: R) -> io::Result { + let mut buf = [0u8; 4]; + reader.read_exact(&mut buf)?; + Ok(u32::from_le_bytes(buf)) +} + +pub fn read_i64_le(mut reader: R) -> io::Result { + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf)?; + Ok(i64::from_le_bytes(buf)) +} + +pub fn read_f64_le(mut reader: R) -> io::Result { + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf)?; + Ok(f64::from_le_bytes(buf)) +} + +pub fn write_u16_le(mut writer: W, val: u16) -> io::Result<()> { + writer.write_all(&val.to_le_bytes()) +} + +pub fn write_u32_le(mut writer: W, val: u32) -> io::Result<()> { + writer.write_all(&val.to_le_bytes()) +} + +pub fn write_i64_le(mut writer: W, val: i64) -> io::Result<()> { + writer.write_all(&val.to_le_bytes()) +} + +pub fn write_f64_le(mut writer: W, val: f64) -> io::Result<()> { + writer.write_all(&val.to_le_bytes()) +} diff --git a/crates/prometeu-core/Cargo.toml b/crates/prometeu-core/Cargo.toml index 82fa4f9f..94a0cc7d 100644 --- a/crates/prometeu-core/Cargo.toml +++ b/crates/prometeu-core/Cargo.toml @@ -6,4 +6,5 @@ license.workspace = true [dependencies] serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" +prometeu-bytecode = { path = "../prometeu-bytecode" } \ 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 dccbfb80..bc9a6a18 100644 --- a/crates/prometeu-core/src/virtual_machine/mod.rs +++ b/crates/prometeu-core/src/virtual_machine/mod.rs @@ -1,12 +1,11 @@ mod virtual_machine; mod value; -mod opcode; mod call_frame; mod scope_frame; mod program; pub mod native_interface; -pub use opcode::OpCode; +pub use prometeu_bytecode::opcode::OpCode; pub use program::Program; pub use value::Value; pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine}; diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index 4d336434..fc25ced0 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -2,9 +2,10 @@ 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; +use prometeu_bytecode::opcode::OpCode; +use prometeu_bytecode::pbc::{self, ConstantPoolEntry}; /// Reason why the Virtual Machine stopped execution during a specific run. /// This allows the system to decide if it should continue execution in the next tick @@ -84,8 +85,16 @@ impl VirtualMachine { // PBC (Prometeu ByteCode) is a binary format that includes a header, // constant pool, and the raw ROM (bytecode). if program_bytes.starts_with(b"PPBC") { - if let Ok((rom, cp)) = self.parse_pbc(&program_bytes) { - self.program = Program::new(rom, cp); + if let Ok(pbc_file) = pbc::parse_pbc(&program_bytes) { + let cp = pbc_file.cp.into_iter().map(|entry| match entry { + ConstantPoolEntry::Int32(v) => Value::Int32(v), + ConstantPoolEntry::Int64(v) => Value::Int64(v), + ConstantPoolEntry::Float64(v) => Value::Float(v), + ConstantPoolEntry::Boolean(v) => Value::Boolean(v), + ConstantPoolEntry::String(v) => Value::String(v), + ConstantPoolEntry::Null => Value::Null, + }).collect(); + self.program = Program::new(pbc_file.rom, cp); } else { // Fallback for raw bytes if PBC parsing fails self.program = Program::new(program_bytes, vec![]); @@ -111,81 +120,6 @@ impl VirtualMachine { self.cycles = 0; self.halted = false; } - - /// Parses the PROMETEU binary format. - /// Format: "PPBC" (4) | CP_COUNT (u32) | CP_ENTRIES[...] | ROM_SIZE (u32) | ROM_BYTES[...] - fn parse_pbc(&self, bytes: &[u8]) -> Result<(Vec, Vec), String> { - let mut cursor = 4; // Skip "PPBC" signature - - // 1. Parse Constant Pool (literals like strings, ints, floats used in the program) - let cp_count = self.read_u32_at(bytes, &mut cursor)? as usize; - let mut cp = Vec::with_capacity(cp_count); - - for _ in 0..cp_count { - if cursor >= bytes.len() { return Err("Unexpected end of PBC".into()); } - let tag = bytes[cursor]; - cursor += 1; - - match tag { - 1 => { // Int64 (64-bit) - let val = self.read_i64_at(bytes, &mut cursor)?; - cp.push(Value::Int64(val)); - } - 2 => { // Float (64-bit) - let val = self.read_f64_at(bytes, &mut cursor)?; - cp.push(Value::Float(val)); - } - 3 => { // Boolean - if cursor >= bytes.len() { return Err("Unexpected end of PBC".into()); } - let val = bytes[cursor] != 0; - cursor += 1; - cp.push(Value::Boolean(val)); - } - 4 => { // String (UTF-8) - let len = self.read_u32_at(bytes, &mut cursor)? as usize; - if cursor + len > bytes.len() { return Err("Unexpected end of PBC".into()); } - let s = String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned(); - cursor += len; - cp.push(Value::String(s)); - } - 5 => { // Int32 (32-bit) - let val = self.read_u32_at(bytes, &mut cursor)? as i32; - cp.push(Value::Int32(val)); - } - _ => cp.push(Value::Null), - } - } - - // 2. Parse ROM (executable bytecode) - let rom_size = self.read_u32_at(bytes, &mut cursor)? as usize; - if cursor + rom_size > bytes.len() { - return Err("Invalid ROM size in PBC".into()); - } - let rom = bytes[cursor..cursor + rom_size].to_vec(); - - Ok((rom, cp)) - } - - fn read_u32_at(&self, bytes: &[u8], cursor: &mut usize) -> Result { - if *cursor + 4 > bytes.len() { return Err("Unexpected end of PBC".into()); } - let val = u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().unwrap()); - *cursor += 4; - Ok(val) - } - - fn read_i64_at(&self, bytes: &[u8], cursor: &mut usize) -> Result { - if *cursor + 8 > bytes.len() { return Err("Unexpected end of PBC".into()); } - let val = i64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().unwrap()); - *cursor += 8; - Ok(val) - } - - fn read_f64_at(&self, bytes: &[u8], cursor: &mut usize) -> Result { - if *cursor + 8 > bytes.len() { return Err("Unexpected end of PBC".into()); } - let val = f64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().unwrap()); - *cursor += 8; - Ok(val) - } } impl Default for VirtualMachine { diff --git a/crates/prometeuc/Cargo.toml b/crates/prometeuc/Cargo.toml new file mode 100644 index 00000000..7e87b37e --- /dev/null +++ b/crates/prometeuc/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "prometeuc" +version = "0.1.0" +edition = "2021" + +[dependencies] +prometeu-bytecode = { path = "../prometeu-bytecode" } diff --git a/crates/prometeuc/src/codegen/mod.rs b/crates/prometeuc/src/codegen/mod.rs new file mode 100644 index 00000000..141c9923 --- /dev/null +++ b/crates/prometeuc/src/codegen/mod.rs @@ -0,0 +1,2 @@ +pub use prometeu_bytecode::opcode::OpCode; +pub use prometeu_bytecode::asm::{Asm, Operand, assemble}; diff --git a/crates/prometeuc/src/main.rs b/crates/prometeuc/src/main.rs new file mode 100644 index 00000000..2e184d62 --- /dev/null +++ b/crates/prometeuc/src/main.rs @@ -0,0 +1,5 @@ +pub mod codegen; + +fn main() { + println!("Prometeu Compiler (stub)"); +} -- 2.47.2