clean up, debugger, add comments
This commit is contained in:
parent
5ac24e5491
commit
a7b824fe21
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
@ -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")),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) = ¶m.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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
20
crates/prometeu-compiler/src/codegen/syscall_map.rs
Normal file
20
crates/prometeu-compiler/src/codegen/syscall_map.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>>,
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@ -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],
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use std::collections::VecDeque;
|
||||
use crate::log::{LogEvent, LogLevel, LogSource};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct LogService {
|
||||
events: VecDeque<LogEvent>,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
mod prometeu_hub;
|
||||
mod window_manager;
|
||||
|
||||
pub use prometeu_hub::PrometeuHub;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use crate::virtual_machine::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Program {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user