dev/vm-improvements (#3)

Co-authored-by: Nilton Constantino <nilton.constantino@visma.com>
Reviewed-on: #3
This commit is contained in:
bquarkz 2026-01-20 10:18:44 +00:00
parent 7e99eed6b4
commit 694d6c8be6
24 changed files with 1477 additions and 194 deletions

5
Cargo.lock generated
View File

@ -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",
]

View File

@ -0,0 +1,7 @@
[package]
name = "prometeu-bytecode"
version = "0.1.0"
edition = "2021"
[dependencies]
# No dependencies for now

View File

@ -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);
}
```

View File

@ -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
}

View File

@ -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<Operand>),
Label(String),
}
pub fn assemble(instructions: &[Asm]) -> Result<Vec<u8>, 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)
}

View File

@ -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<DisasmOperand>,
}
#[derive(Debug, Clone)]
pub enum DisasmOperand {
U32(u32),
I32(i32),
I64(i64),
F64(f64),
Bool(bool),
}
pub fn disasm(rom: &[u8]) -> Result<Vec<Instr>, 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)
}

View File

@ -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);
}
}

View File

@ -1,4 +1,3 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum OpCode {
@ -7,12 +6,19 @@ pub enum OpCode {
Halt = 0x01,
Jmp = 0x02,
JmpIfFalse = 0x03,
JmpIfTrue = 0x04,
Trap = 0x05,
// 6.2 Stack
PushConst = 0x10,
Pop = 0x11,
Dup = 0x12,
Swap = 0x13,
PushI64 = 0x14,
PushF64 = 0x15,
PushBool = 0x16,
PushI32 = 0x17,
PopN = 0x18,
// 6.3 Arithmetic
Add = 0x20,
@ -28,6 +34,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,
@ -60,10 +74,17 @@ impl TryFrom<u16> for OpCode {
0x01 => Ok(OpCode::Halt),
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),
0x13 => Ok(OpCode::Swap),
0x14 => Ok(OpCode::PushI64),
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),
@ -75,6 +96,14 @@ impl TryFrom<u16> 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),
@ -100,10 +129,17 @@ impl OpCode {
OpCode::Halt => 1,
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,
OpCode::PushF64 => 2,
OpCode::PushBool => 2,
OpCode::PushI32 => 2,
OpCode::Add => 2,
OpCode::Sub => 2,
OpCode::Mul => 4,
@ -115,6 +151,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,
@ -126,32 +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(0x10).unwrap(), OpCode::PushConst);
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);
}
}

View File

@ -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<ConstantPoolEntry>,
pub rom: Vec<u8>,
}
pub fn parse_pbc(bytes: &[u8]) -> Result<PbcFile, String> {
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<Vec<u8>, 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)
}

View File

@ -0,0 +1,41 @@
use std::io::{self, Read, Write};
pub fn read_u16_le<R: Read>(mut reader: R) -> io::Result<u16> {
let mut buf = [0u8; 2];
reader.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
pub fn read_u32_le<R: Read>(mut reader: R) -> io::Result<u32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
pub fn read_i64_le<R: Read>(mut reader: R) -> io::Result<i64> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(i64::from_le_bytes(buf))
}
pub fn read_f64_le<R: Read>(mut reader: R) -> io::Result<f64> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(f64::from_le_bytes(buf))
}
pub fn write_u16_le<W: Write>(mut writer: W, val: u16) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
pub fn write_u32_le<W: Write>(mut writer: W, val: u32) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
pub fn write_i64_le<W: Write>(mut writer: W, val: i64) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
pub fn write_f64_le<W: Write>(mut writer: W, val: f64) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}

View File

@ -6,4 +6,5 @@ license.workspace = true
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = "1.0"
prometeu-bytecode = { path = "../prometeu-bytecode" }

View File

@ -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,
};

View File

@ -290,7 +290,7 @@ impl PrometeuOS {
// Helper para syscalls
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<u64, String> {
fn syscall_log_write(&mut self, vm: &mut VirtualMachine, level_val: i64, tag: u16, msg: String) -> Result<u64, String> {
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)
}
@ -471,7 +473,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 +485,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 +496,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 +515,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,14 +523,20 @@ 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();
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)
}
@ -670,13 +687,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
@ -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)),

View File

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

View File

@ -1,11 +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};

View File

@ -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<u64, String>;
}

View File

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

View File

@ -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<Ordering> {
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<f64> {
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<i64> {
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));

View File

@ -1,9 +1,11 @@
use crate::hardware::HardwareBridge;
use crate::prometeu_os::NativeInterface;
use crate::virtual_machine::call_frame::CallFrame;
use crate::virtual_machine::opcode::OpCode;
use crate::virtual_machine::scope_frame::ScopeFrame;
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
@ -42,8 +44,10 @@ pub struct VirtualMachine {
pub pc: usize,
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
pub operand_stack: Vec<Value>,
/// Call Stack: stores execution frames for function calls and local variables.
/// Call Stack: stores execution frames for function calls.
pub call_stack: Vec<CallFrame>,
/// Scope Stack: stores frames for blocks within a function.
pub scope_stack: Vec<ScopeFrame>,
/// Globals: storage for persistent variables that survive between frames.
pub globals: Vec<Value>,
/// The currently loaded program (Bytecode + Constant Pool).
@ -65,6 +69,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(),
@ -80,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![]);
@ -101,82 +114,12 @@ 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;
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<u8>, Vec<Value>), 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 => { // Integer (64-bit)
let val = self.read_i64_at(bytes, &mut cursor)?;
cp.push(Value::Integer(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));
}
_ => 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<u32, String> {
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<i64, String> {
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<f64, String> {
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 {
@ -234,6 +177,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;
@ -307,14 +258,47 @@ 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::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")?;
self.push(val);
}
OpCode::PushI64 => {
let val = self.read_i64()?;
self.push(Value::Int64(val));
}
OpCode::PushI32 => {
let val = self.read_i32()?;
self.push(Value::Int32(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()?;
}
OpCode::PopN => {
let n = self.read_u16()?;
for _ in 0..n {
self.pop()?;
}
}
OpCode::Dup => {
let val = self.peek()?.clone();
self.push(val);
@ -326,40 +310,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))
}
@ -368,22 +387,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)),
@ -401,6 +422,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")?;
@ -437,37 +502,31 @@ 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;
}
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);
// 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
@ -504,7 +563,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;
@ -534,6 +596,40 @@ impl VirtualMachine {
Ok(u32::from_le_bytes(bytes))
}
fn read_i32(&mut self) -> Result<i32, String> {
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<i64, String> {
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<f64, String> {
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<u16, String> {
if self.pc + 2 > self.program.rom.len() {
return Err("Unexpected end of ROM".into());
@ -546,6 +642,15 @@ impl VirtualMachine {
Ok(u16::from_le_bytes(bytes))
}
fn read_u8(&mut self) -> Result<u8, String> {
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 +684,494 @@ 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<u64, String> {
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::Int64(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()));
}
#[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_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::Int64(123));
}
#[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::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::Int64(1));
}
#[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::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));
}
#[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
}
}

View File

@ -0,0 +1,7 @@
[package]
name = "prometeuc"
version = "0.1.0"
edition = "2021"
[dependencies]
prometeu-bytecode = { path = "../prometeu-bytecode" }

View File

@ -0,0 +1,2 @@
pub use prometeu_bytecode::opcode::OpCode;
pub use prometeu_bytecode::asm::{Asm, Operand, assemble};

View File

@ -0,0 +1,5 @@
pub mod codegen;
fn main() {
println!("Prometeu Compiler (stub)");
}

View File

@ -31,7 +31,8 @@ The VM has:
* **PC (Program Counter)** — next instruction
* **Operand Stack** — value stack
* **Call Stack** — frame stack
* **Call Stack** — stores execution frames for function calls
* **Scope Stack** — stores frames for blocks within a function
* **Heap** — dynamic memory
* **Globals** — global variables
* **Constant Pool** — literals and references
@ -60,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
@ -75,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:
@ -122,6 +150,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 |
---
@ -133,6 +162,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 |
---
@ -155,9 +188,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 |
---
@ -174,12 +215,16 @@ 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 |
**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.
---
@ -205,6 +250,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 |