dev/vm-improvements (#3)
Co-authored-by: Nilton Constantino <nilton.constantino@visma.com> Reviewed-on: #3
This commit is contained in:
parent
78485b194c
commit
c8a0444d8e
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
7
crates/prometeu-bytecode/Cargo.toml
Normal file
7
crates/prometeu-bytecode/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "prometeu-bytecode"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# No dependencies for now
|
||||
152
crates/prometeu-bytecode/README.md
Normal file
152
crates/prometeu-bytecode/README.md
Normal 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);
|
||||
}
|
||||
```
|
||||
29
crates/prometeu-bytecode/src/abi.rs
Normal file
29
crates/prometeu-bytecode/src/abi.rs
Normal 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
|
||||
}
|
||||
69
crates/prometeu-bytecode/src/asm.rs
Normal file
69
crates/prometeu-bytecode/src/asm.rs
Normal 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)
|
||||
}
|
||||
64
crates/prometeu-bytecode/src/disasm.rs
Normal file
64
crates/prometeu-bytecode/src/disasm.rs
Normal 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)
|
||||
}
|
||||
51
crates/prometeu-bytecode/src/lib.rs
Normal file
51
crates/prometeu-bytecode/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
111
crates/prometeu-bytecode/src/pbc.rs
Normal file
111
crates/prometeu-bytecode/src/pbc.rs
Normal 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)
|
||||
}
|
||||
41
crates/prometeu-bytecode/src/readwrite.rs
Normal file
41
crates/prometeu-bytecode/src/readwrite.rs
Normal 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())
|
||||
}
|
||||
@ -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" }
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
pub struct CallFrame {
|
||||
pub return_address: usize,
|
||||
pub return_pc: u32,
|
||||
pub stack_base: usize,
|
||||
pub locals_count: usize,
|
||||
}
|
||||
@ -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};
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
3
crates/prometeu-core/src/virtual_machine/scope_frame.rs
Normal file
3
crates/prometeu-core/src/virtual_machine/scope_frame.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub struct ScopeFrame {
|
||||
pub scope_stack_base: usize,
|
||||
}
|
||||
@ -1,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));
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
7
crates/prometeuc/Cargo.toml
Normal file
7
crates/prometeuc/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "prometeuc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
||||
2
crates/prometeuc/src/codegen/mod.rs
Normal file
2
crates/prometeuc/src/codegen/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub use prometeu_bytecode::opcode::OpCode;
|
||||
pub use prometeu_bytecode::asm::{Asm, Operand, assemble};
|
||||
5
crates/prometeuc/src/main.rs
Normal file
5
crates/prometeuc/src/main.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod codegen;
|
||||
|
||||
fn main() {
|
||||
println!("Prometeu Compiler (stub)");
|
||||
}
|
||||
@ -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 |
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user