clean up, debugger, add comments

This commit is contained in:
Nilton Constantino 2026-01-23 06:52:31 +00:00
parent 5ac24e5491
commit a7b824fe21
No known key found for this signature in database
60 changed files with 1033 additions and 557 deletions

View File

@ -1,116 +1,116 @@
# 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).
Official contract (ABI) of the PROMETEU ecosystem. This crate defines the Instruction Set Architecture (ISA), the `.pbc` file format (Prometeu ByteCode), and basic tools for assembly (Assembler) and disassembly (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**.
The PVM (Prometeu Virtual Machine) is a **stack-based** machine. Most instructions operate on values at the top of the operand stack. The default multi-byte data format in ROM is **Little-Endian**.
### Convenção de Notação da Pilha
Nas tabelas abaixo, usamos a seguinte notação para representar o estado da pilha:
### Stack Notation Convention
In the tables below, we use the following notation to represent the state of the stack:
`[a, b] -> [c]`
Significa que a instrução remove `a` e `b` da pilha (onde `b` estava no topo) e insere `c` no topo.
Means the instruction removes `a` and `b` from the stack (where `b` was at the top) and pushes `c` to the top.
---
## Conjunto de Instruções (ISA)
## Instruction Set Architecture (ISA)
### 6.1 Controle de Execução
### 6.1 Execution Control
| OpCode | Valor | Operandos | Pilha | Descrição |
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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). |
| `Nop` | `0x00` | - | `[] -> []` | No operation. |
| `Halt` | `0x01` | - | `[] -> []` | Permanently halts VM execution. |
| `Jmp` | `0x02` | `addr: u32` | `[] -> []` | Unconditional jump to absolute address `addr`. |
| `JmpIfFalse`| `0x03` | `addr: u32` | `[bool] -> []` | Jumps to `addr` if the popped value is `false`. |
| `JmpIfTrue` | `0x04` | `addr: u32` | `[bool] -> []` | Jumps to `addr` if the popped value is `true`. |
| `Trap` | `0x05` | - | `[] -> []` | Debugger interruption (breakpoint). |
### 6.2 Manipulação da Pilha
### 6.2 Stack Manipulation
| OpCode | Valor | Operandos | Pilha | Descrição |
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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. |
| `PushConst` | `0x10` | `idx: u32` | `[] -> [val]` | Loads the constant at index `idx` from the Constant Pool. |
| `Pop` | `0x11` | - | `[val] -> []` | Removes and discards the top value of the stack. |
| `Dup` | `0x12` | - | `[val] -> [val, val]` | Duplicates the value at the top of the stack. |
| `Swap` | `0x13` | - | `[a, b] -> [b, a]` | Swaps the positions of the two values at the top. |
| `PushI64` | `0x14` | `val: i64` | `[] -> [i64]` | Pushes an immediate 64-bit integer. |
| `PushF64` | `0x15` | `val: f64` | `[] -> [f64]` | Pushes an immediate 64-bit floating point. |
| `PushBool` | `0x16` | `val: u8` | `[] -> [bool]` | Pushes a boolean (0=false, 1=true). |
| `PushI32` | `0x17` | `val: i32` | `[] -> [i32]` | Pushes an immediate 32-bit integer. |
| `PopN` | `0x18` | `n: u16` | `[...] -> [...]` | Removes `n` values from the stack at once. |
### 6.3 Aritmética
A VM realiza promoção automática de tipos (ex: `i32` + `f64` resulta em `f64`).
### 6.3 Arithmetic
The VM performs automatic type promotion (e.g., `i32` + `f64` results in `f64`).
| OpCode | Valor | Pilha | Descrição |
| OpCode | Value | Stack | Description |
| :--- | :--- | :--- | :--- |
| `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. |
| `Add` | `0x20` | `[a, b] -> [a + b]` | Adds the two top values. |
| `Sub` | `0x21` | `[a, b] -> [a - b]` | Subtracts `b` from `a`. |
| `Mul` | `0x22` | `[a, b] -> [a * b]` | Multiplies the two top values. |
| `Div` | `0x23` | `[a, b] -> [a / b]` | Divides `a` by `b`. Error if `b == 0`. |
| `Neg` | `0x3E` | `[a] -> [-a]` | Negates the numeric sign. |
### 6.4 Lógica e Comparação
### 6.4 Logic and Comparison
| OpCode | Valor | Pilha | Descrição |
| OpCode | Value | Stack | Description |
| :--- | :--- | :--- | :--- |
| `Eq` | `0x30` | `[a, b] -> [bool]` | Testa igualdade. |
| `Neq` | `0x31` | `[a, b] -> [bool]` | Testa desigualdade. |
| `Eq` | `0x30` | `[a, b] -> [bool]` | Tests equality. |
| `Neq` | `0x31` | `[a, b] -> [bool]` | Tests inequality. |
| `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. |
| `And` | `0x34` | `[a, b] -> [bool]` | Logical AND (boolean). |
| `Or` | `0x35` | `[a, b] -> [bool]` | Logical OR (boolean). |
| `Not` | `0x36` | `[a] -> [!a]` | Logical NOT. |
| `BitAnd` | `0x37` | `[a, b] -> [int]` | Bitwise AND. |
| `BitOr` | `0x38` | `[a, b] -> [int]` | Bitwise OR. |
| `BitXor` | `0x39` | `[a, b] -> [int]` | Bitwise XOR. |
| `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
### 6.5 Variables and Memory
| OpCode | Valor | Operandos | Pilha | Descrição |
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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. |
| `GetGlobal`| `0x40` | `idx: u32` | `[] -> [val]` | Loads value from global `idx`. |
| `SetGlobal`| `0x41` | `idx: u32` | `[val] -> []` | Stores top of stack in global `idx`. |
| `GetLocal` | `0x42` | `idx: u32` | `[] -> [val]` | Loads local `idx` from the current frame. |
| `SetLocal` | `0x43` | `idx: u32` | `[val] -> []` | Stores top of stack in local `idx` of the current frame. |
### 6.6 Funções e Escopo
### 6.6 Functions and Scope
| OpCode | Valor | Operandos | Pilha | Descrição |
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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. |
| `Call` | `0x50` | `addr: u32, args: u32` | `[a1, a2] -> [...]` | Calls `addr`. The `args` values at the top become locals of the new frame. |
| `Ret` | `0x51` | - | `[val] -> [val]` | Returns from the current function, clearing the frame and returning the top value. |
| `PushScope`| `0x52` | - | `[] -> []` | Starts a sub-scope (block) for temporary locals. |
| `PopScope` | `0x53` | - | `[] -> []` | Ends sub-scope, removing locals created in it from the stack. |
### 6.7 Heap (Memória Dinâmica)
### 6.7 Heap (Dynamic Memory)
| OpCode | Valor | Operandos | Pilha | Descrição |
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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`. |
| `Alloc` | `0x60` | `size: u32` | `[] -> [ref]` | Allocates `size` slots on the heap and returns a reference. |
| `LoadRef` | `0x61` | `offset: u32`| `[ref] -> [val]` | Reads value from the heap at address `ref + offset`. |
| `StoreRef`| `0x62` | `offset: u32`| `[ref, val] -> []` | Writes `val` to the heap at address `ref + offset`. |
### 6.8 Sistema e Sincronização
### 6.8 System and Synchronization
| OpCode | Valor | Operandos | Pilha | Descrição |
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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). |
| `Syscall` | `0x70` | `id: u32` | `[...] -> [...]` | Invokes a system/firmware function. The stack depends on the syscall. |
| `FrameSync`| `0x80` | - | `[] -> []` | Marks the end of processing for the current logical frame (60 FPS). |
---
## Estrutura do PBC (Prometeu ByteCode)
## PBC Structure (Prometeu ByteCode)
O PBC é o formato binário oficial para programas Prometeu.
PBC is the official binary format for Prometeu programs.
```rust
// Exemplo de como carregar um arquivo PBC
// Example of how to load a PBC file
let bytes = std::fs::read("game.pbc")?;
let pbc = prometeu_bytecode::pbc::parse_pbc(&bytes)?;
@ -120,11 +120,11 @@ println!("Constants: {}", pbc.cp.len());
---
## Assembler e Disassembler
## Assembler and Disassembler
Este crate fornece ferramentas para facilitar a geração e inspeção de código.
This crate provides tools to facilitate code generation and inspection.
### Montagem (Assembler)
### Assembly (Assembler)
```rust
use prometeu_bytecode::asm::{assemble, Asm, Operand};
use prometeu_bytecode::opcode::OpCode;
@ -139,7 +139,7 @@ let instructions = vec![
let rom_bytes = assemble(&instructions).unwrap();
```
### Desmontagem (Disassembler)
### Disassembly (Disassembler)
```rust
use prometeu_bytecode::disasm::disasm;

View File

@ -1,5 +1,12 @@
//! This module defines the Application Binary Interface (ABI) of the Prometeu Virtual Machine.
//! It specifies how instructions are encoded in bytes and how they interact with memory.
use crate::opcode::OpCode;
/// Returns the size in bytes of the operands for a given OpCode.
///
/// Note: This does NOT include the 2 bytes of the OpCode itself.
/// For example, `PushI32` has a size of 4, but occupies 6 bytes in ROM (2 for OpCode + 4 for value).
pub fn operand_size(opcode: OpCode) -> usize {
match opcode {
OpCode::PushConst => 4,
@ -17,6 +24,7 @@ pub fn operand_size(opcode: OpCode) -> usize {
}
}
/// Checks if an instruction is a jump (branch) instruction.
pub fn is_jump(opcode: OpCode) -> bool {
match opcode {
OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => true,
@ -24,6 +32,7 @@ pub fn is_jump(opcode: OpCode) -> bool {
}
}
/// Checks if an instruction has any immediate operands in the instruction stream.
pub fn has_immediate(opcode: OpCode) -> bool {
operand_size(opcode) > 0
}

View File

@ -2,22 +2,33 @@ use crate::opcode::OpCode;
use crate::readwrite::*;
use std::collections::HashMap;
/// Represents an operand for an instruction.
#[derive(Debug, Clone)]
pub enum Operand {
/// 32-bit unsigned integer (e.g., indices, addresses).
U32(u32),
/// 32-bit signed integer.
I32(i32),
/// 64-bit signed integer.
I64(i64),
/// 64-bit floating point.
F64(f64),
/// Boolean (true/false).
Bool(bool),
/// A symbolic label that will be resolved to an absolute PC address.
Label(String),
}
/// Represents an assembly-level element (either an instruction or a label).
#[derive(Debug, Clone)]
pub enum Asm {
/// An OpCode followed by its operands. The mnemonics represent the operation to be performed.
Op(OpCode, Vec<Operand>),
/// A named marker in the code (e.g., "start:").
Label(String),
}
/// Internal helper to calculate the next PC based on the operands' sizes.
pub fn update_pc_by_operand(initial_pc: u32, operands: &Vec<Operand>) -> u32 {
let mut pcp: u32 = initial_pc;
for operand in operands {
@ -30,6 +41,13 @@ pub fn update_pc_by_operand(initial_pc: u32, operands: &Vec<Operand>) -> u32 {
pcp
}
/// Converts a list of assembly instructions into raw ROM bytes.
///
/// The assembly process is done in two passes:
/// 1. **Label Resolution**: Iterates through all instructions to calculate the PC (Program Counter)
/// of each label and stores them in a map.
/// 2. **Code Generation**: Translates each OpCode and its operands (resolving labels using the map)
/// into the final binary format.
pub fn assemble(instructions: &[Asm]) -> Result<Vec<u8>, String> {
let mut labels = HashMap::new();
let mut current_pc = 0u32;

View File

@ -2,13 +2,18 @@ use crate::opcode::OpCode;
use crate::readwrite::*;
use std::io::{Cursor, Read};
/// Represents a disassembled instruction.
#[derive(Debug, Clone)]
pub struct Instr {
/// The absolute address (PC) where this instruction starts in ROM.
pub pc: u32,
/// The decoded OpCode.
pub opcode: OpCode,
/// The list of operands extracted from the byte stream.
pub operands: Vec<DisasmOperand>,
}
/// Represents an operand decoded from the byte stream.
#[derive(Debug, Clone)]
pub enum DisasmOperand {
U32(u32),
@ -18,6 +23,11 @@ pub enum DisasmOperand {
Bool(bool),
}
/// Translates raw ROM bytes back into a human-readable list of instructions.
///
/// This is the inverse of the assembly process. It reads the bytes sequentially,
/// identifies the OpCode, and then reads the correct number of operand bytes
/// based on the ISA rules.
pub fn disasm(rom: &[u8]) -> Result<Vec<Instr>, String> {
let mut instructions = Vec::new();
let mut cursor = Cursor::new(rom);

View File

@ -1,51 +1,22 @@
//! # Prometeu Bytecode (PBC)
//!
//! This crate defines the core Application Binary Interface (ABI) and Instruction Set Architecture (ISA)
//! for the Prometeu Virtual Machine (PVM).
//!
//! It serves as the "source of truth" for how programs are structured, encoded, and executed
//! within the ecosystem.
//!
//! ## Core Components:
//! - [`opcode`]: Defines the available instructions and their performance characteristics.
//! - [`pbc`]: Handles the serialization and deserialization of `.pbc` files.
//! - [`abi`]: Specifies the binary rules for operands and stack behavior.
//! - [`asm`]: Provides a programmatic Assembler to convert high-level instructions to bytes.
//! - [`disasm`]: Provides a Disassembler to inspect compiled bytecode.
//! - [`readwrite`]: Internal utilities for Little-Endian binary I/O.
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,67 +1,180 @@
/// Represents a single instruction in the Prometeu Virtual Machine.
///
/// Each OpCode is encoded as a 16-bit unsigned integer (u16) in the bytecode.
/// The PVM is a stack-based machine, meaning most instructions take their
/// operands from the top of the stack and push their results back onto it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum OpCode {
// 6.1 Execution Control
// --- 6.1 Execution Control ---
/// No operation. Does nothing for 1 cycle.
Nop = 0x00,
/// Stops the Virtual Machine execution immediately.
Halt = 0x01,
/// Unconditional jump to a specific PC (Program Counter) address.
/// Operand: addr (u32)
Jmp = 0x02,
/// Jumps to `addr` if the value at the top of the stack is `false`.
/// Operand: addr (u32)
/// Stack: [bool] -> []
JmpIfFalse = 0x03,
/// Jumps to `addr` if the value at the top of the stack is `true`.
/// Operand: addr (u32)
/// Stack: [bool] -> []
JmpIfTrue = 0x04,
/// Triggers a software breakpoint. Used for debugging.
Trap = 0x05,
// 6.2 Stack
// --- 6.2 Stack Manipulation ---
/// Loads a constant from the Constant Pool into the stack.
/// Operand: index (u32)
/// Stack: [] -> [value]
PushConst = 0x10,
/// Removes the top value from the stack.
/// Stack: [val] -> []
Pop = 0x11,
/// Duplicates the top value of the stack.
/// Stack: [val] -> [val, val]
Dup = 0x12,
/// Swaps the two top values of the stack.
/// Stack: [a, b] -> [b, a]
Swap = 0x13,
/// Pushes a 64-bit integer literal onto the stack.
/// Operand: value (i64)
PushI64 = 0x14,
/// Pushes a 64-bit float literal onto the stack.
/// Operand: value (f64)
PushF64 = 0x15,
/// Pushes a boolean literal onto the stack (0=false, 1=true).
/// Operand: value (u8)
PushBool = 0x16,
/// Pushes a 32-bit integer literal onto the stack.
/// Operand: value (i32)
PushI32 = 0x17,
/// Removes `n` values from the stack.
/// Operand: n (u16)
PopN = 0x18,
// 6.3 Arithmetic
// --- 6.3 Arithmetic ---
/// Adds the two top values (a + b).
/// Stack: [a, b] -> [result]
Add = 0x20,
/// Subtracts the top value from the second one (a - b).
/// Stack: [a, b] -> [result]
Sub = 0x21,
/// Multiplies the two top values (a * b).
/// Stack: [a, b] -> [result]
Mul = 0x22,
/// Divides the second top value by the top one (a / b).
/// Stack: [a, b] -> [result]
Div = 0x23,
// 6.4 Comparison and Logic
// --- 6.4 Comparison and Logic ---
/// Checks if a equals b.
/// Stack: [a, b] -> [bool]
Eq = 0x30,
/// Checks if a is not equal to b.
/// Stack: [a, b] -> [bool]
Neq = 0x31,
/// Checks if a is less than b.
/// Stack: [a, b] -> [bool]
Lt = 0x32,
/// Checks if a is greater than b.
/// Stack: [a, b] -> [bool]
Gt = 0x33,
/// Logical AND.
/// Stack: [bool, bool] -> [bool]
And = 0x34,
/// Logical OR.
/// Stack: [bool, bool] -> [bool]
Or = 0x35,
/// Logical NOT.
/// Stack: [bool] -> [bool]
Not = 0x36,
/// Bitwise AND.
/// Stack: [int, int] -> [int]
BitAnd = 0x37,
/// Bitwise OR.
/// Stack: [int, int] -> [int]
BitOr = 0x38,
/// Bitwise XOR.
/// Stack: [int, int] -> [int]
BitXor = 0x39,
/// Bitwise Shift Left.
/// Stack: [int, count] -> [int]
Shl = 0x3A,
/// Bitwise Shift Right.
/// Stack: [int, count] -> [int]
Shr = 0x3B,
/// Checks if a is less than or equal to b.
/// Stack: [a, b] -> [bool]
Lte = 0x3C,
/// Checks if a is greater than or equal to b.
/// Stack: [a, b] -> [bool]
Gte = 0x3D,
/// Negates a number (-a).
/// Stack: [num] -> [num]
Neg = 0x3E,
// 6.5 Variables
// --- 6.5 Variables ---
/// Loads a value from a global variable slot.
/// Operand: slot_index (u32)
/// Stack: [] -> [value]
GetGlobal = 0x40,
/// Stores the top value into a global variable slot.
/// Operand: slot_index (u32)
/// Stack: [value] -> []
SetGlobal = 0x41,
/// Loads a value from a local variable slot in the current frame.
/// Operand: slot_index (u32)
/// Stack: [] -> [value]
GetLocal = 0x42,
/// Stores the top value into a local variable slot in the current frame.
/// Operand: slot_index (u32)
/// Stack: [value] -> []
SetLocal = 0x43,
// 6.6 Functions
// --- 6.6 Functions ---
/// Calls a function at a specific address.
/// Operands: addr (u32), args_count (u32)
/// Stack: [arg0, arg1, ...] -> [return_value]
Call = 0x50,
/// Returns from the current function.
/// Stack: [return_val] -> [return_val]
Ret = 0x51,
/// Starts a new local scope (for blocks/loops).
PushScope = 0x52,
/// Ends the current local scope, discarding its local variables.
PopScope = 0x53,
// 6.7 Heap
// --- 6.7 Heap ---
/// Allocates `size` slots on the heap.
/// Stack: [size] -> [reference]
Alloc = 0x60,
/// Reads a value from the heap at `reference + offset`.
/// Operand: offset (u32)
/// Stack: [reference] -> [value]
LoadRef = 0x61,
/// Writes a value to the heap at `reference + offset`.
/// Operand: offset (u32)
/// Stack: [reference, value] -> []
StoreRef = 0x62,
// 6.8 Peripherals and System
// --- 6.8 Peripherals and System ---
/// Invokes a system function (Firmware/OS).
/// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall)
Syscall = 0x70,
/// Synchronizes the VM with the hardware frame (usually 60Hz).
/// Execution pauses until the next VSync.
FrameSync = 0x80,
}
@ -123,6 +236,8 @@ impl TryFrom<u16> for OpCode {
}
impl OpCode {
/// Returns the cost of the instruction in VM cycles.
/// This is used for performance monitoring and resource limiting (Certification).
pub fn cycles(&self) -> u64 {
match self {
OpCode::Nop => 1,

View File

@ -1,22 +1,47 @@
use crate::readwrite::*;
use std::io::{Cursor, Read, Write};
/// An entry in the Constant Pool.
///
/// The Constant Pool is a table of unique values used by the program.
/// Instead of embedding large data (like strings) directly in the instruction stream,
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
#[derive(Debug, Clone, PartialEq)]
pub enum ConstantPoolEntry {
/// Reserved index (0). Represents a null/undefined value.
Null,
/// A 64-bit integer constant.
Int64(i64),
/// A 64-bit floating point constant.
Float64(f64),
/// A boolean constant.
Boolean(bool),
/// A UTF-8 string constant.
String(String),
/// A 32-bit integer constant.
Int32(i32),
}
/// Represents a compiled Prometeu ByteCode (.pbc) file.
///
/// The file format follows this structure (Little-Endian):
/// 1. Magic Header: "PPBC" (4 bytes)
/// 2. CP Count: u32
/// 3. CP Entries: [Tag (u8), Data...]
/// 4. ROM Size: u32
/// 5. ROM Data: [u16 OpCode, Operands...][]
#[derive(Debug, Clone, Default)]
pub struct PbcFile {
/// The list of constants used by the program.
pub cp: Vec<ConstantPoolEntry>,
/// The raw instruction bytes (ROM).
pub rom: Vec<u8>,
}
/// Parses a raw byte buffer into a `PbcFile` structure.
///
/// This function validates the "PPBC" signature and reconstructs the
/// Constant Pool and ROM data from the binary format.
pub fn parse_pbc(bytes: &[u8]) -> Result<PbcFile, String> {
if bytes.len() < 4 || &bytes[0..4] != b"PPBC" {
return Err("Invalid PBC signature".into());
@ -68,6 +93,9 @@ pub fn parse_pbc(bytes: &[u8]) -> Result<PbcFile, String> {
Ok(PbcFile { cp, rom })
}
/// Serializes a `PbcFile` structure into a binary buffer.
///
/// This is used by the compiler to generate the final .pbc file.
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())?;
@ -109,3 +137,48 @@ pub fn write_pbc(pbc: &PbcFile) -> Result<Vec<u8>, String> {
Ok(out)
}
#[cfg(test)]
mod tests {
use crate::asm::{self, Asm, Operand};
use crate::disasm;
use crate::opcode::OpCode;
use crate::pbc::{self, ConstantPoolEntry, PbcFile};
#[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,41 +1,52 @@
//! Binary I/O utilities for the Prometeu ecosystem.
//! All multi-byte data is encoded in Little-Endian format.
use std::io::{self, Read, Write};
/// Reads a 16-bit unsigned integer from a reader.
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))
}
/// Reads a 32-bit unsigned integer from a reader.
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))
}
/// Reads a 64-bit signed integer from a reader.
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))
}
/// Reads a 64-bit floating point from a reader.
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))
}
/// Writes a 16-bit unsigned integer to a writer.
pub fn write_u16_le<W: Write>(mut writer: W, val: u16) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
/// Writes a 32-bit unsigned integer to a writer.
pub fn write_u32_le<W: Write>(mut writer: W, val: u32) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
/// Writes a 64-bit signed integer to a writer.
pub fn write_i64_le<W: Write>(mut writer: W, val: i64) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
/// Writes a 64-bit floating point to a writer.
pub fn write_f64_le<W: Write>(mut writer: W, val: f64) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}

View File

@ -1,40 +0,0 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "prometeu-compiler")]
#[command(about = "Prometeu Compiler", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Builds a Prometeu project
Build {
/// Project directory
project_dir: PathBuf,
/// Entry file
#[arg(short, long)]
entry: Option<PathBuf>,
/// Output PBC file
#[arg(short, long)]
out: Option<PathBuf>,
/// Emit disassembly file
#[arg(long, default_value_t = true)]
emit_disasm: bool,
/// Emit symbols file
#[arg(long, default_value_t = true)]
emit_symbols: bool,
},
/// Verifies a Prometeu project
Verify {
/// Project directory
project_dir: PathBuf,
},
}

View File

@ -1,15 +1,23 @@
use anyhow::{anyhow, Result};
use oxc_ast::ast::*;
use anyhow::{Result, anyhow};
/// Extracts the name of the function being called from a CallExpression.
pub fn get_callee_name(expr: &Expression) -> Result<String> {
get_member_expr_name(expr)
}
/// Recursively extracts a full name from an identifier or a member expression.
///
/// Example:
/// - `foo` -> "foo"
/// - `Color.RED` -> "Color.RED"
/// - `PGfx.drawRect` -> "Gfx.drawRect" (Stripping 'P' prefix if appropriate)
pub fn get_member_expr_name(expr: &Expression) -> Result<String> {
match expr {
Expression::Identifier(ident) => {
let name = ident.name.to_string();
// Remove prefix 'P' if it's followed by an uppercase letter (e.g., PGfx -> Gfx)
// Prometeu SDK uses 'P' prefix for some internal names to avoid conflicts
// with standard JS identifiers. We strip it here to match the ISA names.
if name.len() > 1 && name.starts_with('P') && name.chars().nth(1).unwrap().is_uppercase() {
Ok(name[1..].to_string())
} else {
@ -21,6 +29,6 @@ pub fn get_member_expr_name(expr: &Expression) -> Result<String> {
let prop = member.property.name.to_string();
Ok(format!("{}.{}", obj, prop))
}
_ => Err(anyhow!("Unsupported expression")),
_ => Err(anyhow!("Unsupported expression for name resolution")),
}
}

View File

@ -1,31 +1,47 @@
use anyhow::{Result, anyhow};
use oxc_ast::ast::*;
use oxc_span::{Span, GetSpan};
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::asm::{Asm, Operand, assemble};
use prometeu_bytecode::pbc::{ConstantPoolEntry, PbcFile, write_pbc};
use crate::compiler::Symbol;
use crate::syscall_map;
use crate::codegen::ast_util;
use prometeu_core::prometeu_os::Syscall;
use std::collections::HashMap;
use crate::codegen::{input_map, syscall_map};
use crate::compiler::Symbol;
use anyhow::{anyhow, Result};
use oxc_ast::ast::*;
use oxc_span::{GetSpan, Span};
use prometeu_bytecode::asm;
use prometeu_bytecode::asm::{assemble, Asm, Operand};
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::pbc::{write_pbc, ConstantPoolEntry, PbcFile};
use prometeu_core::hardware::Syscall;
use prometeu_core::model::Color;
use std::collections::HashMap;
/// The core Code Generator for the Prometeu Compiler.
///
/// It maintains the state of the compilation process, including symbol tables,
/// local/global variable mapping, and the constant pool.
pub struct Codegen {
/// Name of the file being compiled (used for debug symbols).
file_name: String,
/// Full source code of the file (used for position lookup).
source_text: String,
/// Collected debug symbols mapping PC to source lines.
pub symbols: Vec<Symbol>,
instructions: Vec<(Asm, bool)>, // (Asm, has_symbol)
/// The stream of generated assembly instructions.
/// The boolean indicates if the instruction should have a debug symbol attached.
instructions: Vec<(Asm, bool)>,
/// Mapping of local variable names to their stack offsets in the current frame.
locals: HashMap<String, u32>,
/// Mapping of global variable names to their slots in the VM's global memory.
globals: HashMap<String, u32>,
/// The Constant Pool, which stores unique values (strings, large numbers).
constant_pool: Vec<ConstantPoolEntry>,
/// Counter for the next available local variable ID.
next_local: u32,
/// Counter for the next available global variable ID.
next_global: u32,
/// Counter for generating unique labels (e.g., for 'if' or 'while' blocks).
label_count: u32,
}
impl Codegen {
/// Creates a new Codegen instance for a specific file.
pub fn new(file_name: String, source_text: String) -> Self {
Self {
file_name,
@ -41,6 +57,8 @@ impl Codegen {
}
}
/// Adds a value to the Constant Pool and returns its index.
/// If the value already exists, it returns the existing index.
fn add_constant(&mut self, entry: ConstantPoolEntry) -> u32 {
if let Some(pos) = self.constant_pool.iter().position(|e| e == &entry) {
pos as u32
@ -51,53 +69,54 @@ impl Codegen {
}
}
/// Entry point for compiling a single Program (AST).
pub fn compile_program(&mut self, program: &Program) -> Result<Vec<u8>> {
self.compile_programs(vec![(self.file_name.clone(), self.source_text.clone(), program)])
}
/// Compiles multiple programs (files) into a single Prometeu executable (.pbc).
///
/// The compilation process follows several steps:
/// 1. Collection of global symbols (functions and variables).
/// 2. Entry point verification (`frame` function).
/// 3. Global variable initialization.
/// 4. Main system loop generation.
/// 5. Compilation of all function bodies.
/// 6. Assembly and symbol resolution.
pub fn compile_programs(&mut self, programs: Vec<(String, String, &Program)>) -> Result<Vec<u8>> {
// First pass: collect all functions and global variables
// --- FIRST PASS: Global Functions and Variables Collection ---
// In this stage, we iterate through all files to register the existence of functions
// and global variables before starting code generation. This allows a
// function to call another that was defined later or in another file.
let mut all_functions = Vec::new();
for (file, source, program) in &programs {
for item in &program.body {
match item {
// Standard function declaration: function foo() { ... }
Statement::FunctionDeclaration(f) => {
all_functions.push((file.clone(), source.clone(), f.as_ref()));
}
// Exported declaration: export function foo() ... or export const x = 1;
Statement::ExportNamedDeclaration(decl) => {
if let Some(Declaration::FunctionDeclaration(f)) = &decl.declaration {
all_functions.push((file.clone(), source.clone(), f.as_ref()));
} else if let Some(Declaration::VariableDeclaration(var)) = &decl.declaration {
for decl in &var.declarations {
if let BindingPattern::BindingIdentifier(ident) = &decl.id {
let name = ident.name.to_string();
if !self.globals.contains_key(&name) {
let id = self.next_global;
self.globals.insert(name, id);
self.next_global += 1;
}
}
}
self.export_global_variable_declarations(&var);
}
}
// Global variable declaration: var x = 1;
Statement::VariableDeclaration(var) => {
for decl in &var.declarations {
if let BindingPattern::BindingIdentifier(ident) = &decl.id {
let name = ident.name.to_string();
if !self.globals.contains_key(&name) {
let id = self.next_global;
self.globals.insert(name, id);
self.next_global += 1;
}
}
}
self.export_global_variable_declarations(&var);
}
_ => {}
}
}
}
// Find frame function (should be in the first program, which is the entry)
// --- ENTRY POINT VERIFICATION ---
// Prometeu requires a function named `frame()` in the main file (the first in the list).
// This function is called automatically every logical frame, which is supposed to
// be close to the tick (60Hz).
let mut frame_fn_name = None;
if let Some((_, _, entry_program)) = programs.first() {
for item in &entry_program.body {
@ -126,7 +145,9 @@ impl Codegen {
let frame_fn_name = frame_fn_name.ok_or_else(|| anyhow!("export function frame() not found in entry file"))?;
// Initialize globals
// --- GLOBAL INITIALIZATION ---
// Here we generate the code that will be executed as soon as the program loads.
// It evaluates the initialization expressions of global variables and stores them.
for (file, source, program) in &programs {
self.file_name = file.clone();
self.source_text = source.clone();
@ -149,10 +170,13 @@ impl Codegen {
let name = ident.name.to_string();
let id = *self.globals.get(&name).unwrap();
if let Some(init) = &decl.init {
// Compiles the initialization expression (e.g., the "10" in "var x = 10")
self.compile_expr(init)?;
} else {
// If there is no initializer, it defaults to 0 (I32)
self.emit_op(OpCode::PushI32, vec![Operand::I32(0)], decl.span);
}
// Stores the resulting value in the global variable slot
self.emit_op(OpCode::SetGlobal, vec![Operand::U32(id)], decl.span);
}
}
@ -160,39 +184,74 @@ impl Codegen {
}
}
// Entry point: loop calling frame
// --- EXECUTION LOOP GENERATION (ENTRY POINT) ---
// After initializing globals, the code enters an infinite loop that:
// 1. Calls the frame() function.
// 2. Clears the return value from the stack (Prometeu ABI requires functions to leave 1 value).
// 3. Synchronizes with hardware (FrameSync - waits for the next VSync).
// 4. Jumps back to the start of the loop.
self.emit_label("entry".to_string());
self.emit_op(OpCode::Call, vec![Operand::Label(frame_fn_name), Operand::U32(0)], Span::default());
self.emit_op(OpCode::Pop, vec![], Span::default());
self.emit_op(OpCode::FrameSync, vec![], Span::default());
self.emit_op(OpCode::Jmp, vec![Operand::Label("entry".to_string())], Span::default());
// Compile all functions
// --- FUNCTION COMPILATION ---
// Compiles the body of each function collected in the first pass.
for (file, source, f) in all_functions {
self.file_name = file;
self.source_text = source;
if let Some(ident) = &f.id {
let name = ident.name.to_string();
// Each function starts with a label with its name to be resolved by Call
self.emit_label(name);
self.compile_function(f)?;
// Ensures the function returns to the caller
self.emit_op(OpCode::Ret, vec![], Span::default());
}
}
// --- FINAL PHASE: ASSEMBLY AND PACKAGING ---
// 1. Converts the list of instructions into raw bytecode
let asm_vec: Vec<Asm> = self.instructions.iter().map(|(a, _)| a.clone()).collect();
let bytecode = assemble(&asm_vec).map_err(|e| anyhow!("Assemble error: {}", e))?;
// Finalize symbols (associate PC)
// 2. Resolves label addresses to real bytecode positions (PC)
self.finalize_symbols();
// 3. Builds the PBC file structure (Constant Pool + Bytecode)
let pbc = PbcFile {
cp: self.constant_pool.clone(),
rom: bytecode,
};
// 4. Serializes to the final binary format
write_pbc(&pbc).map_err(|e| anyhow!("PBC Write error: {}", e))
}
/// Registers a global variable in the symbol table.
/// Global variables are accessible from any function and persist between frames.
fn export_global_variable_declarations(&mut self, var: &VariableDeclaration) {
for decl in &var.declarations {
if let BindingPattern::BindingIdentifier(ident) = &decl.id {
let name = ident.name.to_string();
if !self.globals.contains_key(&name) {
let id = self.next_global;
self.globals.insert(name, id);
self.next_global += 1;
}
}
}
}
/// Compiles a function declaration.
///
/// Functions in Prometeu follow the ABI:
/// 1. Parameters are mapped to the first `n` local slots.
/// 2. `PushScope` is called to protect the caller's environment.
/// 3. The body is compiled sequentially.
/// 4. `PopScope` and `Push Null` are executed before `Ret` to ensure the stack rule.
fn compile_function(&mut self, f: &Function) -> Result<()> {
self.locals.clear();
self.next_local = 0;
@ -200,7 +259,7 @@ impl Codegen {
// Start scope for parameters and local variables
self.emit_op(OpCode::PushScope, vec![], f.span);
// Map parameters to locals
// Map parameters to locals (they are pushed by the caller before the Call instruction)
for param in &f.params.items {
if let BindingPattern::BindingIdentifier(ident) = &param.pattern {
let name = ident.name.to_string();
@ -218,15 +277,15 @@ impl Codegen {
// ABI Rule: Every function MUST leave exactly one value on the stack before RET.
// If the function doesn't have a return statement, we push Null.
// For now, we always push Null at the end if it's not a return.
// In a more complex compiler, we would check if all paths return.
self.emit_op(OpCode::PopScope, vec![], Span::default());
self.emit_op(OpCode::PushConst, vec![Operand::U32(0)], Span::default()); // Index 0 is Null in PBC
Ok(())
}
/// Translates a Statement into bytecode.
fn compile_stmt(&mut self, stmt: &Statement) -> Result<()> {
match stmt {
// var x = 10;
Statement::VariableDeclaration(var) => {
for decl in &var.declarations {
if let BindingPattern::BindingIdentifier(ident) = &decl.id {
@ -234,6 +293,7 @@ impl Codegen {
if let Some(init) = &decl.init {
self.compile_expr(init)?;
} else {
// Default initialization to 0
self.emit_op(OpCode::PushI32, vec![Operand::I32(0)], decl.span);
}
let id = self.next_local;
@ -242,10 +302,13 @@ impl Codegen {
}
}
}
// console.log("hello");
Statement::ExpressionStatement(expr_stmt) => {
self.compile_expr(&expr_stmt.expression)?;
// ABI requires us to Pop unused return values from the stack to prevent leaks
self.emit_op(OpCode::Pop, vec![], expr_stmt.span);
}
// if (a == b) { ... } else { ... }
Statement::IfStatement(if_stmt) => {
let else_label = self.new_label("else");
let end_label = self.new_label("end_if");
@ -263,6 +326,7 @@ impl Codegen {
self.emit_label(end_label);
}
// { let x = 1; }
Statement::BlockStatement(block) => {
self.emit_op(OpCode::PushScope, vec![], block.span);
for stmt in &block.body {
@ -275,8 +339,11 @@ impl Codegen {
Ok(())
}
/// Translates an Expression into bytecode.
/// Expressions always leave exactly one value at the top of the stack.
fn compile_expr(&mut self, expr: &Expression) -> Result<()> {
match expr {
// Literals: push the value directly onto the stack
Expression::NumericLiteral(n) => {
let val = n.value;
if val.fract() == 0.0 && val >= i32::MIN as f64 && val <= i32::MAX as f64 {
@ -295,6 +362,7 @@ impl Codegen {
Expression::NullLiteral(n) => {
self.emit_op(OpCode::PushConst, vec![Operand::U32(0)], n.span);
}
// Variable access: resolve to GetLocal or GetGlobal
Expression::Identifier(ident) => {
let name = ident.name.to_string();
if let Some(&id) = self.locals.get(&name) {
@ -305,13 +373,14 @@ impl Codegen {
return Err(anyhow!("Undefined variable: {} at {:?}", name, ident.span));
}
}
// Assignment: evaluate RHS and store result in LHS slot
Expression::AssignmentExpression(assign) => {
if let AssignmentTarget::AssignmentTargetIdentifier(ident) = &assign.left {
let name = ident.name.to_string();
self.compile_expr(&assign.right)?;
if let Some(&id) = self.locals.get(&name) {
self.emit_op(OpCode::SetLocal, vec![Operand::U32(id)], assign.span);
self.emit_op(OpCode::GetLocal, vec![Operand::U32(id)], assign.span);
self.emit_op(OpCode::GetLocal, vec![Operand::U32(id)], assign.span); // Assignment returns the value
} else if let Some(&id) = self.globals.get(&name) {
self.emit_op(OpCode::SetGlobal, vec![Operand::U32(id)], assign.span);
self.emit_op(OpCode::GetGlobal, vec![Operand::U32(id)], assign.span);
@ -322,6 +391,7 @@ impl Codegen {
return Err(anyhow!("Unsupported assignment target at {:?}", assign.span));
}
}
// Binary operations: evaluate both sides and apply the opcode
Expression::BinaryExpression(bin) => {
self.compile_expr(&bin.left)?;
self.compile_expr(&bin.right)?;
@ -340,6 +410,7 @@ impl Codegen {
};
self.emit_op(op, vec![], bin.span);
}
// Logical operations: evaluate both sides and apply the opcode
Expression::LogicalExpression(log) => {
self.compile_expr(&log.left)?;
self.compile_expr(&log.right)?;
@ -350,6 +421,7 @@ impl Codegen {
};
self.emit_op(op, vec![], log.span);
}
// Unary operations: evaluate argument and apply the opcode
Expression::UnaryExpression(unary) => {
self.compile_expr(&unary.argument)?;
let op = match unary.operator {
@ -360,19 +432,18 @@ impl Codegen {
};
self.emit_op(op, vec![], unary.span);
}
// Function calls: resolve to Syscall or Call
Expression::CallExpression(call) => {
let name = ast_util::get_callee_name(&call.callee)?;
if let Some(syscall_id) = syscall_map::map_syscall(&name) {
if syscall_id == 0xFFFF_FFFF {
// Color.rgb(r, g, b)
// Special case for Color.rgb(r, g, b)
// It's compiled to a sequence of bitwise operations for performance
if call.arguments.len() != 3 {
return Err(anyhow!("Color.rgb expects 3 arguments at {:?}", call.span));
}
// We'll emit the bit manipulation logic here or just a special syscall if we had one.
// Since we have bitwise opcodes, let's use them!
// ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
// Argument 0: r
// Argument 0: r (shift right 3, shift left 11)
if let Some(expr) = call.arguments[0].as_expression() {
self.compile_expr(expr)?;
self.emit_op(OpCode::PushI32, vec![Operand::I32(3)], call.span);
@ -381,7 +452,7 @@ impl Codegen {
self.emit_op(OpCode::Shl, vec![], call.span);
}
// Argument 1: g
// Argument 1: g (shift right 2, shift left 5)
if let Some(expr) = call.arguments[1].as_expression() {
self.compile_expr(expr)?;
self.emit_op(OpCode::PushI32, vec![Operand::I32(2)], call.span);
@ -392,7 +463,7 @@ impl Codegen {
self.emit_op(OpCode::BitOr, vec![], call.span);
// Argument 2: b
// Argument 2: b (shift right 3)
if let Some(expr) = call.arguments[2].as_expression() {
self.compile_expr(expr)?;
self.emit_op(OpCode::PushI32, vec![Operand::I32(3)], call.span);
@ -402,6 +473,7 @@ impl Codegen {
self.emit_op(OpCode::BitOr, vec![], call.span);
} else {
// Standard System Call
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.compile_expr(expr)?;
@ -410,7 +482,7 @@ impl Codegen {
self.emit_op(OpCode::Syscall, vec![Operand::U32(syscall_id)], call.span);
}
} else {
// Local function call
// Local function call (to a function defined in the project)
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.compile_expr(expr)?;
@ -419,10 +491,12 @@ impl Codegen {
self.emit_op(OpCode::Call, vec![Operand::Label(name), Operand::U32(call.arguments.len() as u32)], call.span);
}
}
// Member access (e.g., Color.RED, Pad.A.down)
Expression::StaticMemberExpression(member) => {
let full_name = ast_util::get_member_expr_name(expr)?;
if full_name.to_lowercase().starts_with("color.") {
// Resolved at compile-time to literal values
match full_name.to_lowercase().as_str() {
"color.black" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::BLACK.raw() as i32)], member.span),
"color.white" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::WHITE.raw() as i32)], member.span),
@ -435,15 +509,16 @@ impl Codegen {
"color.orange" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::ORANGE.raw() as i32)], member.span),
"color.indigo" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::INDIGO.raw() as i32)], member.span),
"color.magenta" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::MAGENTA.raw() as i32)], member.span),
"color.colorKey" | "color.color_key" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::COLOR_KEY.raw() as i32)], member.span),
"color.colorkey" | "color.color_key" => self.emit_op(OpCode::PushI32, vec![Operand::I32(Color::COLOR_KEY.raw() as i32)], member.span),
_ => return Err(anyhow!("Unsupported color constant: {} at {:?}", full_name, member.span)),
}
} else if full_name.to_lowercase().starts_with("pad.") {
// Re-mapped to specific input syscalls
let parts: Vec<&str> = full_name.split('.').collect();
if parts.len() == 3 {
let btn_name = parts[1];
let state_name = parts[2];
let btn_id = self.map_btn_name(btn_name)?;
let btn_id = input_map::map_btn_name(btn_name)?;
let syscall_id = match state_name {
"down" => Syscall::InputGetPad as u32,
"pressed" => Syscall::InputGetPadPressed as u32,
@ -457,6 +532,7 @@ impl Codegen {
return Err(anyhow!("Partial Pad access not supported: {} at {:?}", full_name, member.span));
}
} else if full_name.to_lowercase().starts_with("touch.") {
// Re-mapped to specific touch syscalls
let parts: Vec<&str> = full_name.split('.').collect();
match parts.len() {
2 => {
@ -490,39 +566,25 @@ impl Codegen {
Ok(())
}
fn map_btn_name(&self, btn_name: &str) -> Result<u32> {
match btn_name.to_lowercase().as_str() {
"up" => Ok(syscall_map::BTN_UP),
"down" => Ok(syscall_map::BTN_DOWN),
"left" => Ok(syscall_map::BTN_LEFT),
"right" => Ok(syscall_map::BTN_RIGHT),
"a" => Ok(syscall_map::BTN_A),
"b" => Ok(syscall_map::BTN_B),
"x" => Ok(syscall_map::BTN_X),
"y" => Ok(syscall_map::BTN_Y),
"l" => Ok(syscall_map::BTN_L),
"r" => Ok(syscall_map::BTN_R),
"start" => Ok(syscall_map::BTN_START),
"select" => Ok(syscall_map::BTN_SELECT),
_ => Err(anyhow!("Unsupported button: {}", btn_name)),
}
}
/// Generates a new unique label name for control flow.
fn new_label(&mut self, prefix: &str) -> String {
let label = format!("{}_{}", prefix, self.label_count);
self.label_count += 1;
label
}
/// Emits a label definition in the instruction stream.
fn emit_label(&mut self, name: String) {
self.instructions.push((Asm::Label(name), false));
}
/// Emits an OpCode and its operands into the instruction stream.
/// Also records a debug symbol if the source span is available.
fn emit_op(&mut self, opcode: OpCode, operands: Vec<Operand>, span: Span) {
let has_symbol = if !span.is_unspanned() {
let (line, col) = self.lookup_pos(span.start);
self.symbols.push(Symbol {
pc: 0,
pc: 0, // Will be resolved during finalize_symbols
file: self.file_name.clone(),
line,
col,
@ -534,6 +596,7 @@ impl Codegen {
self.instructions.push((Asm::Op(opcode, operands), has_symbol));
}
/// Converts a character position (offset) into line and column numbers.
fn lookup_pos(&self, pos: u32) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
@ -551,6 +614,9 @@ impl Codegen {
(line, col)
}
/// Second pass of symbol resolution.
/// After the final PC for each instruction is known (via `assemble`),
/// this function updates the `pc` field in all debug symbols.
fn finalize_symbols(&mut self) {
let mut current_pc = 0u32;
let mut symbol_ptr = 0;
@ -564,7 +630,7 @@ impl Codegen {
symbol_ptr += 1;
}
current_pc += 2;
current_pc += 2; // OpCode size (u16)
current_pc = asm::update_pc_by_operand(current_pc, operands);
}
}

View File

@ -1,5 +1,18 @@
//! # Codegen Module
//!
//! This module is responsible for translating the High-Level AST into
//! Low-Level Prometeu ByteCode instructions.
//!
//! It consists of:
//! - **codegen.rs**: The main engine that traverses the AST and maintains the compiler state.
//! - **validator.rs**: A pre-flight check to ensure the AST only contains supported constructs.
//! - **syscall_map.rs**: A bridge between high-level JS functions and virtual machine syscall IDs.
//! - **ast_util.rs**: Convenience functions for extracting information from OXC AST nodes.
pub mod codegen;
pub mod validator;
pub mod ast_util;
pub mod syscall_map;
mod input_map;
pub use codegen::Codegen;
pub use codegen::Codegen;

View File

@ -0,0 +1,20 @@
use prometeu_core::hardware::Syscall;
/// Maps a High-Level function name to its corresponding Virtual Machine Syscall ID.
///
/// The Prometeu VM exposes hardware functionality (Graphics, Audio, I/O) through
/// a syscall interface. This function allows the compiler to recognize these
/// calls and emit the `Syscall <id>` opcode.
pub fn map_syscall(name: &str) -> Option<u32> {
// Check if the name matches a standard syscall defined in the core firmware
if let Some(syscall) = Syscall::from_name(name) {
return Some(syscall as u32);
}
// Handle special compiler intrinsics that don't map 1:1 to a single syscall ID.
match name {
// Color.rgb(r,g,b) is expanded to bitwise logic by the codegen
"Color.rgb" | "color.rgb" => Some(0xFFFF_FFFF),
_ => None,
}
}

View File

@ -1,23 +1,35 @@
use oxc_ast::ast::*;
use oxc_ast_visit::{Visit, walk};
use oxc_span::GetSpan;
use anyhow::{Result, anyhow};
use crate::syscall_map;
use crate::codegen::ast_util;
use crate::codegen::syscall_map;
use anyhow::{anyhow, Result};
use oxc_ast::ast::*;
use oxc_ast_visit::{walk, Visit};
use oxc_span::GetSpan;
/// AST Visitor that ensures the source code follows the Prometeu subset of JS/TS.
///
/// Since the Prometeu Virtual Machine is highly optimized and focused on game logic,
/// it does not support the full ECMA-262 specification (e.g., no `async`, `class`,
/// `try/catch`, or `generators`).
pub struct Validator {
/// List of validation errors found during the pass.
errors: Vec<String>,
/// Set of function names defined in the project, used to distinguish
/// local calls from invalid references.
local_functions: std::collections::HashSet<String>,
}
impl Validator {
/// Performs a full validation pass on a program.
///
/// Returns `Ok(())` if the program is valid, or a combined error message
/// listing all violations.
pub fn validate(program: &Program) -> Result<()> {
let mut validator = Self {
errors: Vec::new(),
local_functions: std::collections::HashSet::new(),
};
// 1. Collect all local function names
// 1. Discovery Pass: Collect all function names and imports
for item in &program.body {
match item {
Statement::FunctionDeclaration(f) => {
@ -53,7 +65,7 @@ impl Validator {
}
}
// 2. Recursive validation of the AST
// 2. Traversal Pass: Check every node for compatibility
validator.visit_program(program);
if validator.errors.is_empty() {
@ -65,6 +77,7 @@ impl Validator {
}
impl<'a> Visit<'a> for Validator {
/// Validates that only supported statements are used.
fn visit_statement(&mut self, stmt: &Statement<'a>) {
match stmt {
Statement::VariableDeclaration(_) |
@ -74,15 +87,16 @@ impl<'a> Visit<'a> for Validator {
Statement::ExportNamedDeclaration(_) |
Statement::ImportDeclaration(_) |
Statement::FunctionDeclaration(_) => {
// Supported
// These are the only statements the PVM handles currently.
walk::walk_statement(self, stmt);
}
_ => {
self.errors.push(format!("Unsupported statement type at {:?}", stmt.span()));
self.errors.push(format!("Unsupported statement type at {:?}. Note: Prometeu does not support while/for loops or classes yet.", stmt.span()));
}
}
}
/// Validates that only supported expressions are used.
fn visit_expression(&mut self, expr: &Expression<'a>) {
match expr {
Expression::NumericLiteral(_) |
@ -96,11 +110,11 @@ impl<'a> Visit<'a> for Validator {
Expression::UnaryExpression(_) |
Expression::CallExpression(_) |
Expression::StaticMemberExpression(_) => {
// Base types supported, detailed checks in specific visit methods if needed
// Basic JS logic is supported.
walk::walk_expression(self, expr);
}
_ => {
self.errors.push(format!("Unsupported expression type at {:?}", expr.span()));
self.errors.push(format!("Unsupported expression type at {:?}. Note: Closures, arrow functions, and object literals are not yet supported.", expr.span()));
}
}
}

View File

@ -1,43 +1,62 @@
use std::path::{Path, PathBuf};
use anyhow::{Result, Context, anyhow};
use crate::codegen::validator::Validator;
use crate::codegen::Codegen;
use anyhow::{anyhow, Context, Result};
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_parser::Parser;
use oxc_span::SourceType;
use oxc_ast::ast::*;
use crate::codegen::Codegen;
use crate::codegen::validator::Validator;
use std::fs;
use std::collections::{HashMap, VecDeque};
use prometeu_bytecode::disasm::disasm;
use serde::Serialize;
use std::collections::{HashMap, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
/// Represents a debug symbol that maps a Program Counter (PC) to a specific
/// source code location.
#[derive(Serialize)]
pub struct Symbol {
/// The absolute address in the bytecode.
pub pc: u32,
/// The original source file path.
pub file: String,
/// 1-based line number.
pub line: usize,
/// 1-based column number.
pub col: usize,
}
/// The result of a successful compilation process.
/// It contains the final binary and the metadata needed for debugging.
pub struct CompilationUnit {
/// The raw binary data (PBC format).
pub rom: Vec<u8>,
/// The list of debug symbols for source mapping.
pub symbols: Vec<Symbol>,
}
impl CompilationUnit {
/// Writes the compilation results to the disk.
///
/// # Arguments
/// * `out` - The target path for the .pbc file.
/// * `emit_disasm` - If true, generates a .disasm.txt file next to the output.
/// * `emit_symbols` - If true, generates a symbols.json file for debuggers.
pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> {
// 1. Save the main binary
fs::write(out, &self.rom).with_context(|| format!("Failed to write PBC to {:?}", out))?;
// 2. Export symbols for the HostDebugger
if emit_symbols {
let symbols_path = out.with_file_name("symbols.json");
let symbols_json = serde_json::to_string_pretty(&self.symbols)?;
fs::write(&symbols_path, symbols_json)?;
}
// 3. Export human-readable disassembly for developer inspection
if emit_disasm {
let disasm_path = out.with_extension("disasm.txt");
// Try to parse as PBC, if fails use raw
// Extract the actual bytecode (stripping the PBC header if present)
let rom_to_disasm = if let Ok(pbc) = prometeu_bytecode::pbc::parse_pbc(&self.rom) {
pbc.rom
} else {
@ -48,6 +67,7 @@ impl CompilationUnit {
let mut disasm_text = String::new();
for instr in instructions {
// Find a matching symbol to show which source line generated this instruction
let symbol = self.symbols.iter().find(|s| s.pc == instr.pc);
let comment = if let Some(s) = symbol {
format!(" ; {}:{}", s.file, s.line)
@ -69,8 +89,11 @@ impl CompilationUnit {
}
}
/// Helper to resolve import paths (e.g., converting './utils' to './utils.ts').
fn resolve_import(base_path: &Path, import_str: &str) -> Result<PathBuf> {
let mut path = base_path.parent().unwrap().join(import_str);
// Auto-append extensions if missing
if !path.exists() {
if path.with_extension("ts").exists() {
path.set_extension("ts");
@ -78,36 +101,48 @@ fn resolve_import(base_path: &Path, import_str: &str) -> Result<PathBuf> {
path.set_extension("js");
}
}
if !path.exists() {
return Err(anyhow!("Cannot resolve import '{}' from {:?}", import_str, base_path));
}
Ok(path.canonicalize()?)
}
/// Orchestrates the compilation of a Prometeu project starting from an entry file.
///
/// This function:
/// 1. Crawls the import graph to find all dependencies.
/// 2. Parses each file using `oxc`.
/// 3. Validates that the code adheres to Prometeu's restricted JS/TS subset.
/// 4. Hands over all programs to the `Codegen` for binary generation.
pub fn compile(entry: &Path) -> Result<CompilationUnit> {
let allocator = Allocator::default();
let mut modules = HashMap::new();
let mut queue = VecDeque::new();
// Start with the entry point
let entry_abs = entry.canonicalize()
.with_context(|| format!("Failed to canonicalize entry path: {:?}", entry))?;
queue.push_back(entry_abs.clone());
// --- PHASE 1: Dependency Resolution and Parsing ---
while let Some(path) = queue.pop_front() {
let path_str = path.to_string_lossy().to_string();
if modules.contains_key(&path_str) {
continue;
continue; // Already processed
}
let source_text = fs::read_to_string(&path)
.with_context(|| format!("Failed to read file: {:?}", path))?;
// Allocate source_text in the allocator to keep it alive
// We use an Allocator to manage the lifetime of AST nodes and source strings efficiently
let source_text_ptr: &str = allocator.alloc_str(&source_text);
let source_type = SourceType::from_path(&path).unwrap_or_default();
let parser_ret = Parser::new(&allocator, source_text_ptr, source_type).parse();
// Handle syntax errors immediately
if !parser_ret.errors.is_empty() {
for error in parser_ret.errors {
eprintln!("{:?}", error);
@ -115,10 +150,11 @@ pub fn compile(entry: &Path) -> Result<CompilationUnit> {
return Err(anyhow!("Failed to parse module: {:?}", path));
}
// Validate individual module
// --- PHASE 2: Individual Module Validation ---
// Checks for things like 'class' or 'async' which are not supported by PVM.
Validator::validate(&parser_ret.program)?;
// Find imports to add to queue
// Discover new imports to crawl
for item in &parser_ret.program.body {
if let Statement::ImportDeclaration(decl) = item {
let import_path = decl.source.value.as_str();
@ -130,14 +166,15 @@ pub fn compile(entry: &Path) -> Result<CompilationUnit> {
modules.insert(path_str, (source_text_ptr, parser_ret.program));
}
// --- PHASE 3: Code Generation ---
let entry_str = entry_abs.to_string_lossy().to_string();
let mut program_list = Vec::new();
// Add entry program first
// Ensure the entry module (containing 'frame()') is processed first
let entry_data = modules.get(&entry_str).ok_or_else(|| anyhow!("Entry module not found after loading"))?;
program_list.push((entry_str.clone(), entry_data.0.to_string(), &entry_data.1));
// Add all other programs
// Collect all other modules
for (path, (source, program)) in &modules {
if path != &entry_str {
program_list.push((path.clone(), source.to_string(), program));

View File

@ -1,16 +1,75 @@
pub mod cli;
//! # Prometeu Compiler
//!
//! This crate provides the official compiler for the Prometeu ecosystem.
//! It translates TypeScript/JavaScript source code into Prometeu ByteCode (.pbc).
//!
//! ## Workflow:
//! 1. **Parsing**: Uses the `oxc` parser to generate an Abstract Syntax Tree (AST).
//! 2. **Validation**: Checks for unsupported features and ensures ABI compliance.
//! 3. **Codegen**: Traverses the AST and emits `prometeu-bytecode` instructions.
//! 4. **Assembly**: Resolves labels and serializes the program into the binary PBC format.
//!
//! ## Example Usage:
//! ```bash
//! # Build a project
//! prometeu-compiler build ./my-game --entry ./src/main.ts --out ./game.pbc
//! ```
pub mod codegen;
pub mod compiler;
pub mod syscall_map;
use clap::Parser;
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
/// Command line interface for the Prometeu Compiler.
#[derive(Parser)]
#[command(name = "prometeu-compiler")]
#[command(version, about = "Official compiler for the PROMETEU Virtual Machine", long_about = None)]
pub struct Cli {
/// The action to perform (build or verify).
#[command(subcommand)]
pub command: Commands,
}
/// Available subcommands for the compiler.
#[derive(Subcommand)]
pub enum Commands {
/// Builds a Prometeu project by compiling source code into a PBC file.
Build {
/// Path to the project root directory.
project_dir: PathBuf,
/// Explicit path to the entry file (defaults to src/main.ts).
#[arg(short, long)]
entry: Option<PathBuf>,
/// Path to save the compiled .pbc file.
#[arg(short, long)]
out: Option<PathBuf>,
/// Whether to generate a .disasm file for debugging.
#[arg(long, default_value_t = true)]
emit_disasm: bool,
/// Whether to generate a .json symbols file for source mapping.
#[arg(long, default_value_t = true)]
emit_symbols: bool,
},
/// Verifies if a Prometeu project is syntactically and semantically valid without emitting code.
Verify {
/// Path to the project root directory.
project_dir: PathBuf,
},
}
/// Main entry point for the compiler library's execution logic.
/// Parses CLI arguments and dispatches to the appropriate compiler functions.
pub fn run() -> Result<()> {
let cli = cli::Cli::parse();
let cli = Cli::parse();
match cli.command {
cli::Commands::Build {
Commands::Build {
project_dir,
entry,
out,
@ -32,7 +91,7 @@ pub fn run() -> Result<()> {
let compilation_unit = compiler::compile(&entry)?;
compilation_unit.export(&out, emit_disasm, emit_symbols)?;
}
cli::Commands::Verify { project_dir } => {
Commands::Verify { project_dir } => {
let entry = project_dir.join("src/main.ts");
println!("Verifying project at {:?}", project_dir);
println!("Entry: {:?}", entry);

View File

@ -1,5 +1,7 @@
use anyhow::Result;
/// Main entry point for the Prometeu Compiler binary.
/// It delegates execution to the library's `run` function.
fn main() -> Result<()> {
prometeu_compiler::run()
}

View File

@ -1,27 +0,0 @@
use prometeu_core::prometeu_os::Syscall;
use prometeu_core::model::ButtonId;
pub const BTN_UP: u32 = ButtonId::Up as u32;
pub const BTN_DOWN: u32 = ButtonId::Down as u32;
pub const BTN_LEFT: u32 = ButtonId::Left as u32;
pub const BTN_RIGHT: u32 = ButtonId::Right as u32;
pub const BTN_A: u32 = ButtonId::A as u32;
pub const BTN_B: u32 = ButtonId::B as u32;
pub const BTN_X: u32 = ButtonId::X as u32;
pub const BTN_Y: u32 = ButtonId::Y as u32;
pub const BTN_L: u32 = ButtonId::L as u32;
pub const BTN_R: u32 = ButtonId::R as u32;
pub const BTN_START: u32 = ButtonId::Start as u32;
pub const BTN_SELECT: u32 = ButtonId::Select as u32;
pub fn map_syscall(name: &str) -> Option<u32> {
if let Some(syscall) = Syscall::from_name(name) {
return Some(syscall as u32);
}
// Fallback para nomes especiais do compilador
match name {
"Color.rgb" | "color.rgb" => Some(0xFFFF_FFFF), // ID especial para Color.rgb (não é um syscall real)
_ => None,
}
}

View File

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use crate::model::AppMode;
use serde::{Deserialize, Serialize};
use crate::virtual_machine::Value;
@ -80,6 +80,7 @@ pub enum DebugEvent {
vm_steps: u32,
syscalls: u32,
cycles: u64,
cycles_budget: u64,
host_cpu_time_us: u64,
violations: u32,
gfx_used_bytes: usize,
@ -103,6 +104,30 @@ mod tests {
use super::*;
use crate::virtual_machine::Value;
#[test]
fn test_telemetry_event_serialization() {
let event = DebugEvent::Telemetry {
frame_index: 10,
vm_steps: 100,
syscalls: 5,
cycles: 5000,
cycles_budget: 10000,
host_cpu_time_us: 1200,
violations: 0,
gfx_used_bytes: 1024,
gfx_inflight_bytes: 0,
gfx_slots_occupied: 1,
audio_used_bytes: 2048,
audio_inflight_bytes: 0,
audio_slots_occupied: 2,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"event\":\"telemetry\""));
assert!(json.contains("\"cycles\":5000"));
assert!(json.contains("\"cycles_budget\":10000"));
}
#[test]
fn test_get_state_serialization() {
let resp = DebugResponse::GetState {

View File

@ -11,21 +11,31 @@ use crate::telemetry::CertificationConfig;
/// PROMETEU Firmware.
///
/// The central orchestrator of the system. It manages the high-level state machine,
/// transitioning between system states like booting, the Hub launcher, and
/// actual application execution.
/// The central orchestrator of the console. The firmware acts as the
/// "Control Unit", managing the high-level state machine of the system.
///
/// It is responsible for transitioning between different modes of operation,
/// such as showing the splash screen, running the Hub (launcher), or
/// executing a game/app.
///
/// ### Execution Loop:
/// The firmware is designed to be ticked once per frame (60Hz). During each
/// tick, it:
/// 1. Updates peripherals with the latest input signals.
/// 2. Delegates the logic update to the current active state.
/// 3. Handles state transitions (e.g., from Loading to Playing).
pub struct Firmware {
/// The Virtual Machine instance.
/// The execution engine (PVM) for user applications.
pub vm: VirtualMachine,
/// The System Operating logic (syscalls, telemetry, logs).
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
pub os: PrometeuOS,
/// The System UI / Launcher environment.
/// The internal state of the system launcher (Hub).
pub hub: PrometeuHub,
/// Current high-level state of the system.
/// The current operational state (e.g., Reset, SplashScreen, GameRunning).
pub state: FirmwareState,
/// Desired execution target resolved at boot.
/// The desired application to run after boot (Hub or specific Cartridge).
pub boot_target: BootTarget,
/// Tracking flag to ensure `on_enter` is called exactly once per state transition.
/// State-machine lifecycle tracker.
state_initialized: bool,
}

View File

@ -1,10 +1,10 @@
pub use crate::firmware::firmware_step_crash_screen::AppCrashesStep;
pub use crate::firmware::firmware_step_game_running::GameRunningStep;
pub use crate::firmware::firmware_step_hub_home::HubHomeStep;
pub use crate::firmware::firmware_step_launch_hub::LaunchHubStep;
pub use crate::firmware::firmware_step_load_cartridge::LoadCartridgeStep;
pub use crate::firmware::firmware_step_reset::ResetStep;
pub use crate::firmware::firmware_step_splash_screen::SplashScreenStep;
pub use crate::firmware::firmware_step_launch_hub::LaunchHubStep;
pub use crate::firmware::firmware_step_hub_home::HubHomeStep;
pub use crate::firmware::firmware_step_load_cartridge::LoadCartridgeStep;
pub use crate::firmware::firmware_step_game_running::GameRunningStep;
pub use crate::firmware::firmware_step_crash_screen::AppCrashesStep;
#[derive(Debug, Clone)]
pub enum FirmwareState {

View File

@ -1,7 +1,7 @@
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext;
use crate::model::Color;
use crate::log::{LogLevel, LogSource};
use crate::model::Color;
#[derive(Debug, Clone)]
pub struct AppCrashesStep {

View File

@ -1,8 +1,8 @@
use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, HubHomeStep, LoadCartridgeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use crate::model::CartridgeLoader;
use crate::log::{LogLevel, LogSource};
use crate::model::CartridgeLoader;
#[derive(Debug, Clone)]
pub struct LaunchHubStep;

View File

@ -1,7 +1,7 @@
use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use crate::model::{AppMode, Cartridge, Color, Rect};
use crate::log::{LogLevel, LogSource};
use crate::model::{AppMode, Cartridge, Color, Rect};
#[derive(Debug, Clone)]
pub struct LoadCartridgeStep {

View File

@ -11,7 +11,7 @@ pub(crate) mod firmware_step_game_running;
pub(crate) mod firmware_step_crash_screen;
mod prometeu_context;
pub use boot_target::BootTarget;
pub use firmware::Firmware;
pub use firmware_state::FirmwareState;
pub use prometeu_context::PrometeuContext;
pub use boot_target::BootTarget;

View File

@ -4,8 +4,8 @@ mod fs_entry;
mod fs_backend;
mod virtual_fs;
pub use fs_backend::FsBackend;
pub use fs_entry::FsEntry;
pub use fs_error::FsError;
pub use fs_state::FsState;
pub use fs_entry::FsEntry;
pub use fs_backend::FsBackend;
pub use virtual_fs::VirtualFS;

View File

@ -1,6 +1,15 @@
use crate::fs::{FsBackend, FsEntry, FsError};
/// Virtual Filesystem (VFS) interface for Prometeu.
///
/// The VFS provides a sandboxed, unified path interface for user applications.
/// Instead of interacting directly with the host's disk, the VM uses
/// normalized paths (e.g., `/user/save.dat`).
///
/// The actual storage is provided by an `FsBackend`, which can be a real
/// directory on disk, an in-memory map, or even a network resource.
pub struct VirtualFS {
/// The active storage implementation.
backend: Option<Box<dyn FsBackend>>,
}

View File

@ -1,5 +1,5 @@
use crate::hardware::memory_banks::{TileBankPoolInstaller, SoundBankPoolInstaller};
use crate::model::{AssetEntry, BankStats, BankType, Color, HandleId, LoadStatus, SlotRef, SlotStats, TileBank, TileSize, SoundBank, Sample, PreloadEntry};
use crate::hardware::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
use crate::model::{AssetEntry, BankStats, BankType, Color, HandleId, LoadStatus, PreloadEntry, Sample, SlotRef, SlotStats, SoundBank, TileBank, TileSize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
@ -659,7 +659,7 @@ impl AssetManager {
#[cfg(test)]
mod tests {
use super::*;
use crate::hardware::memory_banks::{TileBankPoolAccess, SoundBankPoolAccess, MemoryBanks};
use crate::hardware::memory_banks::{MemoryBanks, SoundBankPoolAccess, TileBankPoolAccess};
#[test]
fn test_asset_loading_flow() {

View File

@ -97,15 +97,25 @@ pub enum AudioCommand {
/// PROMETEU Audio Subsystem.
///
/// Models a multi-channel PCM sampler.
/// It works like an "Audio CPU": the Game Core sends high-level commands
/// every frame, and the Host backend implements the low-level mixer.
/// Models a multi-channel PCM sampler (SPU).
/// The audio system in Prometeu is **command-based**. This means the Core
/// doesn't generate raw audio samples; instead, it sends high-level commands
/// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend
/// (e.g., CPAL on desktop) then consumes these commands and performs the
/// actual mixing at the native hardware sample rate.
///
/// ### Key Features:
/// - **16 Simultaneous Voices**: Independent volume, pan, and pitch.
/// - **Sample-based Synthesis**: Plays PCM data stored in SoundBanks.
/// - **Stereo Output**: 48kHz output target.
pub struct Audio {
/// Local state of the 16 hardware voices.
/// Local state of the hardware voices. This state is used for logic
/// (e.g., checking if a sound is still playing) and is synchronized with the Host.
pub voices: [Channel; MAX_CHANNELS],
/// Queue of pending commands to be processed by the Host mixer.
/// This queue is typically flushed and sent to the Host once per frame.
pub commands: Vec<AudioCommand>,
/// Interface to access sound memory banks.
/// Interface to access sound memory banks (ARAM).
pub sound_banks: Arc<dyn SoundBankPoolAccess>,
}

View File

@ -4,18 +4,24 @@ use std::sync::Arc;
/// Blending modes inspired by classic 16-bit hardware.
/// Defines how source pixels are combined with existing pixels in the framebuffer.
///
/// ### Usage Example:
/// ```rust
/// // Draw a semi-transparent blue rectangle
/// gfx.fill_rect_blend(10, 10, 50, 50, Color::BLUE, BlendMode::Half);
/// ```
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BlendMode {
/// No blending: source overwrites destination.
/// No blending: a source overwrites the destination.
#[default]
None,
/// Average: dst = (src + dst) / 2. Creates a semi-transparent effect.
/// Average: `dst = (src + dst) / 2`. Creates a semi-transparent effect.
Half,
/// Additive: dst = dst + (src / 2). Good for glows/light.
/// Additive: `dst = clamp(dst + (src / 2))`. Good for glows/light.
HalfPlus,
/// Subtractive: dst = dst - (src / 2). Good for shadows.
/// Subtractive: `dst = clamp(dst - (src / 2))`. Good for shadows.
HalfMinus,
/// Full Additive: dst = dst + src. Saturated light effect.
/// Full Additive: `dst = clamp(dst + src)`. Saturated light effect.
Full,
}
@ -24,44 +30,48 @@ pub enum BlendMode {
/// Models a specialized graphics chip with a fixed resolution, double buffering,
/// and a multi-layered tile/sprite architecture.
///
/// Composition Order (back to front):
/// 1. Sprites with priority 0 (optional background objects)
/// 2. Tile Layer 0 + Sprites with priority 1
/// 3. Tile Layer 1 + Sprites with priority 2
/// 4. Tile Layer 2 + Sprites with priority 3
/// 5. Tile Layer 3 + Sprites with priority 4
/// 6. Scene Fade effect
/// 7. HUD Layer (always on top)
/// 8. HUD Fade effect
/// The GFX system works by composing several "layers" into a single 16-bit
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
/// and specialized console features like background scrolling and sprite sorting.
///
/// ### Layer Composition Order (back to front):
/// 1. **Priority 0 Sprites**: Objects behind everything else.
/// 2. **Tile Layer 0 + Priority 1 Sprites**: Background 0.
/// 3. **Tile Layer 1 + Priority 2 Sprites**: Background 1.
/// 4. **Tile Layer 2 + Priority 3 Sprites**: Background 2.
/// 5. **Tile Layer 3 + Priority 4 Sprites**: Foreground.
/// 6. **Scene Fade**: Global brightness/color filter.
/// 7. **HUD Layer**: Fixed UI elements (always on top).
/// 8. **HUD Fade**: Independent fade for the UI.
pub struct Gfx {
/// Width of the internal framebuffer.
/// Width of the internal framebuffer in pixels.
w: usize,
/// Height of the internal framebuffer.
/// Height of the internal framebuffer in pixels.
h: usize,
/// Front buffer: the one currently being displayed by the Host.
/// Front buffer: the "VRAM" currently being displayed by the Host window.
front: Vec<u16>,
/// Back buffer: the one being drawn to during the current frame.
/// Back buffer: the "Work RAM" where new frames are composed.
back: Vec<u16>,
/// 4 scrollable backgrounds.
/// 4 scrollable background layers. Each can have its own scroll (X, Y) and TileBank.
pub layers: [ScrollableTileLayer; 4],
/// 1 fixed layer for User Interface.
/// 1 fixed layer for User Interface (HUD). It doesn't scroll.
pub hud: HudTileLayer,
/// Interface to access graphical memory banks.
/// Shared access to graphical memory banks (tiles and palettes).
pub tile_banks: Arc<dyn TileBankPoolAccess>,
/// Hardware sprites (Object Attribute Memory equivalent).
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
pub sprites: [Sprite; 512],
/// Current transparency level for the main scene (0=invisible, 31=fully visible).
/// Scene brightness/fade level (0 = black/invisible, 31 = fully visible).
pub scene_fade_level: u8,
/// Color used for the scene fade effect.
/// Target color for the scene fade effect (usually Black).
pub scene_fade_color: Color,
/// Current transparency level for the HUD (independent of scene).
/// HUD brightness/fade level (independent from the scene).
pub hud_fade_level: u8,
/// Color used for the HUD fade effect.
/// Target color for the HUD fade effect.
pub hud_fade_color: Color,
/// Internal cache used to sort sprites by priority without allocations.
/// Internal cache used to sort sprites into priority groups to optimize rendering.
priority_buckets: [Vec<usize>; 5],
}

View File

@ -1,23 +1,29 @@
use crate::hardware::{AssetManager, Audio, Gfx, HardwareBridge, Pad, Touch, MemoryBanks};
use crate::hardware::memory_banks::{TileBankPoolAccess, TileBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller};
use crate::hardware::memory_banks::{SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess, TileBankPoolInstaller};
use crate::hardware::{AssetManager, Audio, Gfx, HardwareBridge, MemoryBanks, Pad, Touch};
use std::sync::Arc;
/// Aggregate structure for all virtual hardware peripherals.
///
/// This struct represents the "Mainboard" of the PROMETEU console,
/// containing instances of GFX, Audio, Input (Pad), and Touch.
/// This struct represents the "Mainboard" of the PROMETEU console.
/// It acts as a container for all hardware subsystems. In the Prometeu
/// architecture, hardware is decoupled from the OS and VM, allowing
/// for easier testing and different host implementations (Desktop, Web, etc.).
///
/// ### Console Specifications:
/// - **Resolution**: 320x180 (16:9 Aspect Ratio).
/// - **Color Depth**: RGB565 (16-bit).
/// - **Audio**: Stereo, Command-based mixing.
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
pub struct Hardware {
/// Shared memory banks for hardware assets.
pub memory_banks: Arc<MemoryBanks>,
/// The Graphics Processing Unit.
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
pub gfx: Gfx,
/// The Sound Processing Unit.
/// The Sound Processing Unit (SPU). Manages sample playback and volume control.
pub audio: Audio,
/// The standard digital gamepad.
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
pub pad: Pad,
/// The absolute pointer input device.
/// The absolute pointer input device (Mouse/Touchscreen).
pub touch: Touch,
/// The Asset Management system.
/// The Asset Management system (DMA). Handles loading data into VRAM (TileBanks) and ARAM (SoundBanks).
pub assets: AssetManager,
}
@ -48,7 +54,6 @@ impl Hardware {
pub fn new() -> Self {
let memory_banks = Arc::new(MemoryBanks::new());
Self {
memory_banks: Arc::clone(&memory_banks),
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>),
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
pad: Pad::default(),

View File

@ -1,5 +1,5 @@
use crate::model::{SoundBank, TileBank};
use std::sync::{Arc, RwLock};
use crate::model::{TileBank, SoundBank};
/// Non-generic interface for peripherals to access graphical tile banks.
pub trait TileBankPoolAccess: Send + Sync {

View File

@ -6,6 +6,7 @@ mod input_signal;
mod audio;
mod memory_banks;
pub mod hardware;
pub mod syscalls;
pub use crate::model::HandleId;
pub use asset::AssetManager;
@ -15,6 +16,7 @@ pub use gfx::Gfx;
pub use input_signal::InputSignals;
pub use memory_banks::MemoryBanks;
pub use pad::Pad;
pub use syscalls::Syscall;
pub use touch::Touch;
pub trait HardwareBridge {

View File

@ -1,5 +1,5 @@
use crate::model::Button;
use crate::hardware::input_signal::InputSignals;
use crate::model::Button;
#[derive(Default, Clone, Copy, Debug)]
pub struct Pad {

View File

@ -1,57 +1,106 @@
/// Enumeration of all System Calls (Syscalls) available in the Prometeu environment.
///
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
/// to interact with the outside world (Hardware, OS, Filesystem).
///
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
/// - **0x0xxx**: System & OS Control
/// - **0x1xxx**: Graphics (GFX)
/// - **0x2xxx**: Input (Gamepad & Touch)
/// - **0x3xxx**: Audio (PCM & Mixing)
/// - **0x4xxx**: Filesystem (Sandboxed I/O)
/// - **0x5xxx**: Logging & Debugging
/// - **0x6xxx**: Asset Loading & Memory Banks
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Syscall {
// System
// --- System ---
/// Checks if a cartridge is currently inserted in the virtual slot.
SystemHasCart = 0x0001,
/// Requests the OS to launch a specific cartridge.
SystemRunCart = 0x0002,
// GFX
// --- GFX (Graphics) ---
/// Fills the entire back buffer with a single color.
GfxClear = 0x1001,
/// Draws a solid rectangle.
GfxFillRect = 0x1002,
/// Draws a 1-pixel wide line.
GfxDrawLine = 0x1003,
/// Draws a circle outline.
GfxDrawCircle = 0x1004,
/// Draws a filled circle (disc).
GfxDrawDisc = 0x1005,
/// Draws a rectangle outline.
GfxDrawSquare = 0x1006,
/// Configures one of the 512 hardware sprites.
GfxSetSprite = 0x1007,
// Input
// --- Input ---
/// Returns the current raw state of the digital gamepad (bitmask).
InputGetPad = 0x2001,
/// Returns buttons that were pressed exactly in this frame.
InputGetPadPressed = 0x2002,
/// Returns buttons that were released exactly in this frame.
InputGetPadReleased = 0x2003,
/// Returns how many frames a button has been held down.
InputGetPadHold = 0x2004,
/// Returns the X coordinate of the touch/mouse pointer.
TouchGetX = 0x2101,
/// Returns the Y coordinate of the touch/mouse pointer.
TouchGetY = 0x2102,
/// Returns true if the pointer is currently touching the screen.
TouchIsDown = 0x2103,
/// Returns true if the touch started in this frame.
TouchIsPressed = 0x2104,
/// Returns true if the touch ended in this frame.
TouchIsReleased = 0x2105,
/// Returns how many frames the pointer has been held down.
TouchGetHold = 0x2106,
// Audio
// --- Audio ---
/// Starts playback of a sound sample by its Bank and ID.
AudioPlaySample = 0x3001,
/// Low-level audio play command.
AudioPlay = 0x3002,
// FS
// --- FS (Filesystem) ---
/// Opens a file for reading or writing. Returns a File Handle (u32).
FsOpen = 0x4001,
/// Reads data from an open file handle into the VM heap.
FsRead = 0x4002,
/// Writes data from the VM heap into an open file handle.
FsWrite = 0x4003,
/// Closes an open file handle.
FsClose = 0x4004,
/// Lists entries in a directory.
FsListDir = 0x4005,
/// Checks if a file or directory exists.
FsExists = 0x4006,
/// Deletes a file or empty directory.
FsDelete = 0x4007,
// Log
// --- Log ---
/// Writes a generic string to the system log.
LogWrite = 0x5001,
/// Writes a string to the system log with a specific numerical tag.
LogWriteTag = 0x5002,
// Asset
// --- Asset (DMA) ---
/// Starts an asynchronous load of a file into a memory bank.
AssetLoad = 0x6001,
/// Returns the status of a pending asset load (0=Loading, 1=Ready, 2=Error).
AssetStatus = 0x6002,
/// Finalizes the asset loading, making it available for GFX/Audio.
AssetCommit = 0x6003,
/// Cancels a pending asset load.
AssetCancel = 0x6004,
// Bank
// --- Bank (Memory) ---
/// Returns information about a specific Memory Bank.
BankInfo = 0x6101,
/// Returns information about a slot within a Memory Bank.
BankSlotInfo = 0x6102,
}

View File

@ -1,5 +1,5 @@
use crate::model::Button;
use crate::hardware::input_signal::InputSignals;
use crate::model::Button;
#[derive(Default, Clone, Copy, Debug)]
pub struct Touch {

View File

@ -1,3 +1,20 @@
//! # Prometeu Core
//!
//! This crate contains the engine and hardware definitions for the Prometeu Virtual Console.
//! It serves as the bridge between the physical host (desktop/web) and the virtualized
//! environment where games and apps run.
//!
//! ## Main Components:
//! - [`hardware`]: Virtual peripheral definitions (Graphics, Audio, Input).
//! - [`virtual_machine`]: The stack-based execution engine for Prometeu ByteCode.
//! - [`prometeu_os`]: Resource management, filesystem access, and syscall dispatching.
//! - [`firmware`]: System state machine (Boot, Hub, Game Loading, Crash Handler).
//! - [`model`]: Common data structures (Colors, Sprites, Cartridges).
//! - [`fs`]: Virtualized filesystem abstraction for sandboxed access.
//! - [`log`]: Centralized logging and telemetry service.
//!
//! The "Source of Truth" for the console's behavior lives here.
pub mod hardware;
pub mod log;
pub mod virtual_machine;

View File

@ -1,5 +1,5 @@
use std::collections::VecDeque;
use crate::log::{LogEvent, LogLevel, LogSource};
use std::collections::VecDeque;
pub struct LogService {
events: VecDeque<LogEvent>,

View File

@ -3,7 +3,7 @@ mod log_source;
mod log_event;
mod log_service;
pub use log_level::LogLevel;
pub use log_source::LogSource;
pub use log_event::LogEvent;
pub use log_level::LogLevel;
pub use log_service::LogService;
pub use log_source::LogSource;

View File

@ -1,6 +1,6 @@
use crate::model::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use std::fs;
use std::path::Path;
use crate::model::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
pub struct CartridgeLoader;

View File

@ -1,10 +1,13 @@
/// Simple RGB565 color (0bRRRRRGGGGGGBBBBB).
/// - 5 bits Red
/// - 6 bits Green
/// - 5 bits Blue
/// Represents a 16-bit color in the RGB565 format.
///
/// The RGB565 format is a common high-color representation for embedded systems:
/// - **Red**: 5 bits (0..31)
/// - **Green**: 6 bits (0..63)
/// - **Blue**: 5 bits (0..31)
///
/// There is no alpha channel.
/// Transparency is handled via Color Key or Blend Mode.
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
/// skips during rendering.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Color(pub u16);

View File

@ -11,7 +11,7 @@ mod cartridge;
mod cartridge_loader;
mod window;
pub use asset::{AssetEntry, BankType, BankStats, LoadStatus, SlotRef, SlotStats, HandleId, PreloadEntry};
pub use asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
pub use button::{Button, ButtonId};
pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError};
pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader};

View File

@ -1,5 +1,5 @@
use crate::model::Tile;
use crate::model::tile_bank::TileSize;
use crate::model::Tile;
use crate::model::TileSize::Size8;
pub struct TileMap {

View File

@ -1,3 +1,4 @@
mod prometeu_hub;
mod window_manager;
pub use prometeu_hub::PrometeuHub;

View File

@ -1,55 +1,9 @@
use crate::hardware::HardwareBridge;
use crate::log::{LogLevel, LogSource};
use crate::model::{Color, Rect, Window, WindowId};
use crate::model::{Color, Rect};
use crate::prometeu_hub::window_manager::WindowManager;
use crate::prometeu_os::PrometeuOS;
/// PROMETEU Window Manager.
pub struct WindowManager {
pub windows: Vec<Window>,
pub focused: Option<WindowId>,
}
impl WindowManager {
pub fn new() -> Self {
Self {
windows: Vec::new(),
focused: None,
}
}
pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId {
let id = WindowId(self.windows.len() as u32);
let window = Window {
id,
viewport,
has_focus: false,
title,
color,
};
self.windows.push(window);
id
}
pub fn remove_window(&mut self, id: WindowId) {
self.windows.retain(|w| w.id != id);
if self.focused == Some(id) {
self.focused = None;
}
}
pub fn remove_all_windows(&mut self) {
self.windows.clear();
self.focused = None;
}
pub fn set_focus(&mut self, id: WindowId) {
self.focused = Some(id);
for window in &mut self.windows {
window.has_focus = window.id == id;
}
}
}
/// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub {
pub window_manager: WindowManager,
@ -103,42 +57,4 @@ impl PrometeuHub {
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Rect;
#[test]
fn test_window_manager_focus() {
let mut wm = WindowManager::new();
let id1 = wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
let id2 = wm.add_window("Window 2".to_string(), Rect { x: 10, y: 10, w: 10, h: 10 }, Color::WHITE);
assert_eq!(wm.windows.len(), 2);
assert_eq!(wm.focused, None);
wm.set_focus(id1);
assert_eq!(wm.focused, Some(id1));
assert!(wm.windows[0].has_focus);
assert!(!wm.windows[1].has_focus);
wm.set_focus(id2);
assert_eq!(wm.focused, Some(id2));
assert!(!wm.windows[0].has_focus);
assert!(wm.windows[1].has_focus);
}
#[test]
fn test_window_manager_remove_window() {
let mut wm = WindowManager::new();
let id = wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
wm.set_focus(id);
assert_eq!(wm.focused, Some(id));
wm.remove_window(id);
assert_eq!(wm.windows.len(), 0);
assert_eq!(wm.focused, None);
}
}

View File

@ -1,6 +1,4 @@
mod prometeu_os;
pub mod syscalls;
use crate::virtual_machine::NativeInterface;
pub use prometeu_os::PrometeuOS;
pub use syscalls::Syscall;
pub use crate::virtual_machine::native_interface::NativeInterface;

View File

@ -1,62 +1,72 @@
use crate::fs::{FsBackend, FsState, VirtualFS};
use crate::hardware::syscalls::Syscall;
use crate::hardware::{HardwareBridge, InputSignals};
use crate::log::{LogLevel, LogService, LogSource};
use crate::model::{BankType, Cartridge, Color};
use crate::prometeu_os::{NativeInterface, Syscall};
use crate::prometeu_os::NativeInterface;
use crate::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use crate::virtual_machine::{Value, VirtualMachine};
use std::collections::HashMap;
use std::time::Instant;
/// PrometeuOS (POS): The system firmware/base.
/// PrometeuOS (POS): The central system firmware and resource manager.
///
/// Maximum authority for boot, peripherals, PVM execution, and fault handling.
/// It acts as the bridge between the high-level PVM applications and the virtual hardware.
/// POS is the "kernel" of the Prometeu console. It manages the lifecycle of
/// applications, handles hardware abstraction through Syscalls, and enforces
/// resource limits (CAP).
///
/// ### Core Responsibilities:
/// - **Syscall Dispatching**: Implements the `NativeInterface` to connect the VM to hardware.
/// - **Resource Management**: Manages file handles, memory bank status, and cycle budgeting.
/// - **Filesystem Sandboxing**: Provides a virtualized filesystem (VFS) to the VM.
/// - **Telemetry & Certification**: Tracks performance and ensures apps stay within their budget.
/// - **Logging**: Provides a centralized log service for system and app events.
pub struct PrometeuOS {
/// Incremented on every host tick (60Hz).
/// Host Tick Index: Incremented on every host hardware update (usually 60Hz).
pub tick_index: u64,
/// Incremented every time a logical game frame is successfully completed.
/// Logical Frame Index: Incremented only when an app successfully completes a full frame via `FRAME_SYNC`.
pub logical_frame_index: u64,
/// Indicates if there is an ongoing logical frame that hasn't reached `FRAME_SYNC` yet.
/// Execution State: True if the VM is currently mid-frame.
pub logical_frame_active: bool,
/// Number of virtual cycles still available for the current logical frame.
/// Cycle Budget: The number of PVM cycles remaining for the current logical frame.
pub logical_frame_remaining_cycles: u64,
/// Real-world CPU time (in microseconds) consumed by the last host tick.
/// Performance Metric: Time spent by the host CPU processing the last tick (in microseconds).
pub last_frame_cpu_time_us: u64,
// Filesystem
/// The virtual filesystem interface.
// --- Filesystem ---
/// The virtual filesystem interface, abstracting the physical storage.
pub fs: VirtualFS,
/// Current state of the FS (Mounted, Error, etc).
/// Current health and connection status of the virtual filesystem.
pub fs_state: FsState,
/// Mapping of numeric handles to file paths for open files.
/// Active file handles mapping IDs to their virtual paths.
pub open_files: HashMap<u32, String>,
/// Sequential handle generator for file operations.
/// Generator for unique, non-zero file handles.
pub next_handle: u32,
// Log Service
// --- Logging & Identity ---
/// The centralized service for recording and retrieving system logs.
pub log_service: LogService,
/// Unique ID of the currently running application.
/// Metadata for the currently running application.
pub current_app_id: u32,
pub current_cartridge_title: String,
pub current_cartridge_app_version: String,
pub current_cartridge_app_mode: crate::model::AppMode,
/// Rate-limiting tracker for application logs to prevent performance degradation.
/// Rate-limiter to prevent apps from flooding the log buffer and killing performance.
pub logs_written_this_frame: HashMap<u32, u32>,
// Telemetry and Certification
/// Accumulator for metrics of the frame currently being executed.
// --- Monitoring & Debugging ---
/// Running counters for the current execution slice.
pub telemetry_current: TelemetryFrame,
/// Snapshot of the metrics from the last completed logical frame.
/// The results of the last successfully completed logical frame.
pub telemetry_last: TelemetryFrame,
/// Logic for evaluating compliance with the active CAP profile.
/// Logic for validating that the app obeys the console's Certification (CAP).
pub certifier: Certifier,
/// Global pause flag (used by debugger or system events).
/// Pause state: When true, `tick()` will not advance the VM.
pub paused: bool,
/// Request from debugger to run exactly one instruction or frame.
/// Debugging flag to execute exactly one instruction or frame regardless of budget.
pub debug_step_request: bool,
/// Instant when the system was initialized.
/// Wall-clock time of system startup.
boot_time: Instant,
}
@ -197,6 +207,7 @@ impl PrometeuOS {
// Reset telemetry for the new logical frame
self.telemetry_current = TelemetryFrame {
frame_index: self.logical_frame_index,
cycles_budget: self.certifier.config.cycles_budget_per_frame.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
..Default::default()
};
}
@ -263,7 +274,7 @@ impl PrometeuOS {
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
// Update bank telemetry in current frame (snapshot)
// Update bank telemetry in the current frame (snapshot)
let gfx_stats = hw.assets().bank_info(BankType::TILES);
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
@ -277,6 +288,7 @@ impl PrometeuOS {
// If the frame ended exactly in this tick, we update the final real time in the latch.
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget;
self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes;
self.telemetry_last.gfx_inflight_bytes = self.telemetry_current.gfx_inflight_bytes;
self.telemetry_last.gfx_slots_occupied = self.telemetry_current.gfx_slots_occupied;
@ -460,6 +472,27 @@ mod tests {
assert!(cycles_after_tick_2 > cycles_after_tick_1, "VM should have consumed more cycles because FrameSync reset the budget");
}
#[test]
fn test_telemetry_cycles_budget() {
let mut os = PrometeuOS::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
// Standard budget
os.tick(&mut vm, &signals, &mut hw);
assert_eq!(os.telemetry_current.cycles_budget, PrometeuOS::CYCLES_PER_LOGICAL_FRAME);
// Custom budget via CAP
let mut config = CertificationConfig::default();
config.enabled = true;
config.cycles_budget_per_frame = Some(50000);
let mut os_custom = PrometeuOS::new(Some(config));
os_custom.tick(&mut vm, &signals, &mut hw);
assert_eq!(os_custom.telemetry_current.cycles_budget, 50000);
}
#[test]
fn test_get_color_logic() {
let os = PrometeuOS::new(None);

View File

@ -5,6 +5,7 @@ pub struct TelemetryFrame {
pub frame_index: u64,
pub vm_steps: u32,
pub cycles_used: u64,
pub cycles_budget: u64,
pub syscalls: u32,
pub host_cpu_time_us: u64,
pub violations: u32,

View File

@ -3,10 +3,20 @@ mod value;
mod call_frame;
mod scope_frame;
mod program;
pub mod native_interface;
pub use prometeu_bytecode::opcode::OpCode;
use crate::hardware::HardwareBridge;
pub use program::Program;
pub use prometeu_bytecode::opcode::OpCode;
pub use value::Value;
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, 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

@ -1,13 +0,0 @@
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

@ -1,5 +1,5 @@
use std::sync::Arc;
use crate::virtual_machine::Value;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct Program {

View File

@ -1,15 +1,28 @@
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
/// Represents any piece of data that can be stored on the VM stack or in globals.
///
/// The PVM is "dynamically typed" at the bytecode level, meaning a single
/// `Value` enum can hold different primitive types. The VM performs
/// automatic type promotion (e.g., adding an Int32 to a Float64 results
/// in a Float64) to ensure mathematical correctness.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
/// 32-bit signed integer. Used for most loop counters and indices.
Int32(i32),
/// 64-bit signed integer. Used for large numbers and timestamps.
Int64(i64),
/// 64-bit double precision float. Used for physics and complex math.
Float(f64),
/// Boolean value (true/false).
Boolean(bool),
/// UTF-8 string. Strings are immutable and usually come from the Constant Pool.
String(String),
Ref(usize), // Heap reference
/// A pointer to an object on the heap.
Ref(usize),
/// Represents the absence of a value (equivalent to `null` or `undefined`).
Null,
}

View File

@ -1,9 +1,8 @@
use crate::hardware::HardwareBridge;
use crate::prometeu_os::NativeInterface;
use crate::virtual_machine::call_frame::CallFrame;
use crate::virtual_machine::scope_frame::ScopeFrame;
use crate::virtual_machine::value::Value;
use crate::virtual_machine::Program;
use crate::virtual_machine::{NativeInterface, Program};
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::pbc::{self, ConstantPoolEntry};
@ -37,28 +36,41 @@ pub struct BudgetReport {
/// The PVM (PROMETEU Virtual Machine).
///
/// A deterministic, stack-based virtual machine designed for educational purposes.
/// It models execution through fixed-cost cycles and explicit memory management.
/// A deterministic, stack-based virtual machine designed for game logic and
/// educational simulation. The PVM executes bytecode compiled from TypeScript/JS
/// and interacts with virtual hardware through a specialized instruction set.
///
/// ### Architecture Highlights:
/// - **Stack-Based**: Most operations pop values from the stack and push results back.
/// - **Deterministic**: Execution is cycle-aware, allowing for precise performance budgeting.
/// - **Sandboxed**: No direct access to the host system; all I/O goes through Syscalls.
/// - **Type-Aware**: Supports integers, floats, booleans, and strings with automatic promotion.
///
/// ### Memory Regions:
/// - **ROM**: Immutable instruction storage.
/// - **Operand Stack**: Fast-access temporary storage for calculations.
/// - **Global Pool**: Persistent storage for cross-frame variables.
/// - **Heap**: Dynamic memory for complex data (simplified version).
pub struct VirtualMachine {
/// Program Counter: points to the next byte in the ROM to be executed.
/// Program Counter (PC): The absolute byte offset in ROM for the next instruction.
pub pc: usize,
/// Operand Stack: used for intermediate calculations and passing arguments to opcodes.
/// Operand Stack: The primary workspace for all mathematical and logical operations.
pub operand_stack: Vec<Value>,
/// Call Stack: stores execution frames for function calls.
/// Call Stack: Manages function call context (return addresses, frame limits).
pub call_stack: Vec<CallFrame>,
/// Scope Stack: stores frames for blocks within a function.
/// Scope Stack: Handles block-level local variable visibility (scopes).
pub scope_stack: Vec<ScopeFrame>,
/// Globals: storage for persistent variables that survive between frames.
/// Global Variable Store: Variables that persist for the lifetime of the program.
pub globals: Vec<Value>,
/// The currently loaded program (Bytecode + Constant Pool).
/// The loaded executable (Bytecode + Constant Pool), that is the ROM translated.
pub program: Program,
/// Dynamic memory region for complex structures (Simplified in current version).
/// Heap Memory: Dynamic allocation pool.
pub heap: Vec<Value>,
/// Total accumulated execution cycles since the last reset.
/// Total virtual cycles consumed since the VM started.
pub cycles: u64,
/// Flag indicating if the VM has been stopped by a `HALT` instruction.
/// Stop flag: true if a `HALT` opcode was encountered.
pub halted: bool,
/// A set of PC addresses where execution should pause.
/// Set of ROM addresses used for software breakpoints in the debugger.
pub breakpoints: std::collections::HashSet<usize>,
}
@ -692,7 +704,6 @@ impl VirtualMachine {
mod tests {
use super::*;
use crate::hardware::HardwareBridge;
use crate::prometeu_os::NativeInterface;
use crate::virtual_machine::Value;
struct MockNative;
@ -997,7 +1008,7 @@ mod tests {
// 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)"
// "Don't touch intermediate scopes (they should have already been closed)"
// If they were closed, scope_stack would be empty for this frame.
}

View File

@ -32,7 +32,7 @@ mod tests {
#[test]
fn test_load_cap_config() {
let content = "cycles_budget=500\nmax_syscalls=10\n# comentário\nmax_host_cpu_us=2000";
let content = "cycles_budget=500\nmax_syscalls=10\n# comment\nmax_host_cpu_us=2000";
let path = "test_cap.cfg";
std::fs::write(path, content).unwrap();

View File

@ -1,9 +1,9 @@
use prometeu_core::firmware::{BootTarget, Firmware};
use prometeu_core::Hardware;
use prometeu_core::model::CartridgeLoader;
use prometeu_core::debugger_protocol::*;
use std::net::{TcpListener, TcpStream};
use prometeu_core::firmware::{BootTarget, Firmware};
use prometeu_core::model::CartridgeLoader;
use prometeu_core::Hardware;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
/// Host-side implementation of the PROMETEU DevTools Protocol.
///
@ -276,6 +276,7 @@ impl HostDebugger {
vm_steps: tel.vm_steps,
syscalls: tel.syscalls,
cycles: tel.cycles_used,
cycles_budget: tel.cycles_budget,
host_cpu_time_us: tel.host_cpu_time_us,
violations: tel.violations,
gfx_used_bytes: tel.gfx_used_bytes,

View File

@ -1,6 +1,6 @@
use std::path::PathBuf;
use std::fs;
use prometeu_core::fs::{FsBackend, FsEntry, FsError};
use std::fs;
use std::path::PathBuf;
pub struct HostDirBackend {
root: PathBuf,
@ -85,8 +85,8 @@ impl FsBackend for HostDirBackend {
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::env;
use std::fs;
fn get_temp_dir(name: &str) -> PathBuf {
let mut path = env::temp_dir();

View File

@ -1,8 +1,8 @@
use prometeu_core::hardware::InputSignals;
use prometeu_core::Hardware;
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::Window;
use prometeu_core::hardware::InputSignals;
use prometeu_core::Hardware;
pub struct HostInputHandler {
pub signals: InputSignals,

View File

@ -8,11 +8,11 @@ pub mod input;
pub mod cap;
pub mod utilities;
use runner::HostRunner;
use cap::load_cap_config;
use winit::event_loop::EventLoop;
use prometeu_core::firmware::BootTarget;
use clap::Parser;
use prometeu_core::firmware::BootTarget;
use runner::HostRunner;
use winit::event_loop::EventLoop;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]

View File

@ -1,12 +1,12 @@
use crate::audio::HostAudio;
use crate::fs_backend::HostDirBackend;
use crate::log_sink::HostConsoleSink;
use crate::debugger::HostDebugger;
use crate::stats::HostStats;
use crate::fs_backend::HostDirBackend;
use crate::input::HostInputHandler;
use crate::log_sink::HostConsoleSink;
use crate::stats::HostStats;
use crate::utilities::draw_rgb565_to_rgba8;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_core::firmware::{BootTarget, Firmware};
use prometeu_core::Hardware;
use std::time::{Duration, Instant};
@ -128,12 +128,18 @@ impl HostRunner {
let color_bg = prometeu_core::model::Color::INDIGO; // Dark blue to stand out
let color_warn = prometeu_core::model::Color::RED;
self.hardware.gfx.fill_rect(5, 5, 140, 100, color_bg);
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg);
self.hardware.gfx.draw_text(10, 10, &format!("FPS: {:.1}", self.stats.current_fps), color_text);
self.hardware.gfx.draw_text(10, 18, &format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0), color_text);
self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text);
self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text);
self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}", tel.cycles_used), color_text);
let cycles_pct = if tel.cycles_budget > 0 {
(tel.cycles_used as f64 / tel.cycles_budget as f64) * 100.0
} else {
0.0
};
self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct), color_text);
self.hardware.gfx.draw_text(10, 50, &format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied), color_text);
if tel.gfx_inflight_bytes > 0 {
@ -337,10 +343,10 @@ impl ApplicationHandler for HostRunner {
#[cfg(test)]
mod tests {
use super::*;
use prometeu_core::firmware::BootTarget;
use prometeu_core::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
use std::net::TcpStream;
use prometeu_core::firmware::BootTarget;
use std::io::{Read, Write};
use std::net::TcpStream;
#[test]
fn test_debug_port_opens() {

View File

@ -52,7 +52,7 @@
{
"event": "telemetry",
"fields": [
"frame_index", "vm_steps", "syscalls", "cycles", "host_cpu_time_us", "violations",
"frame_index", "vm_steps", "syscalls", "cycles", "cycles_budget", "host_cpu_time_us", "violations",
"gfx_used_bytes", "gfx_inflight_bytes", "gfx_slots_occupied",
"audio_used_bytes", "audio_inflight_bytes", "audio_slots_occupied"
]