realign and clean up

This commit is contained in:
bQUARKz 2026-02-18 01:47:02 +00:00
parent 90ecd77031
commit f75c61004c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
33 changed files with 228 additions and 1635 deletions

View File

@ -1,6 +1,6 @@
[workspace]
members = [
"crates/compiler/prometeu-bytecode",
"crates/console/prometeu-bytecode",
"crates/console/prometeu-drivers",
"crates/console/prometeu-firmware",

View File

@ -1,152 +0,0 @@
# prometeu-bytecode
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
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**.
### Stack Notation Convention
In the tables below, we use the following notation to represent the state of the stack:
`[a, b] -> [c]`
Means the instruction removes `a` and `b` from the stack (where `b` was at the top) and pushes `c` to the top.
---
## Instruction Set Architecture (ISA)
### 6.1 Execution Control
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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 Stack Manipulation
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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 Arithmetic
The VM performs automatic type promotion (e.g., `i32` + `f64` results in `f64`).
| OpCode | Value | Stack | Description |
| :--- | :--- | :--- | :--- |
| `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 Logic and Comparison
| OpCode | Value | Stack | Description |
| :--- | :--- | :--- | :--- |
| `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]` | 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 Variables and Memory
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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 Functions and Scope
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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 (Dynamic Memory)
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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 System and Synchronization
| OpCode | Value | Operands | Stack | Description |
| :--- | :--- | :--- | :--- | :--- |
| `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). |
---
## PBC Structure (Prometeu ByteCode)
PBC is the official binary format for Prometeu programs.
```rust
// Example of how to load a PBC file
let bytes = std::fs::read("game.pbc")?;
let pbc = prometeu_bytecode::pbc::parse_pbc(&bytes)?;
println!("ROM size: {} bytes", pbc.rom.len());
println!("Constants: {}", pbc.cp.len());
```
---
## Assembler and Disassembler
This crate provides tools to facilitate code generation and inspection.
### Assembly (Assembler)
```rust
use prometeu_bytecode::asm::{assemble, Asm, Operand};
use prometeu_bytecode::opcode::OpCode;
let instructions = vec![
Asm::Op(OpCode::PushI32, vec![Operand::I32(10)]),
Asm::Op(OpCode::PushI32, vec![Operand::I32(20)]),
Asm::Op(OpCode::Add, vec![]),
Asm::Op(OpCode::Halt, vec![]),
];
let rom_bytes = assemble(&instructions).unwrap();
```
### Disassembly (Disassembler)
```rust
use prometeu_bytecode::disasm::disasm;
let rom = vec![/* ... bytes ... */];
let code = disasm(&rom);
for instr in code {
println!("{:04X}: {:?} {:?}", instr.pc, instr.opcode, instr.operands);
}
```

View File

@ -1,147 +0,0 @@
//! 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;
use crate::opcode_spec::OpCodeSpecExt;
/// 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 {
opcode.spec().imm_bytes as usize
}
// --- HIP Trap Codes ---
/// Attempted to access a gate that does not exist or has been recycled incorrectly.
pub const TRAP_INVALID_GATE: u32 = 0x01;
/// Attempted to access a gate that has been explicitly released (RC=0).
pub const TRAP_DEAD_GATE: u32 = 0x02;
/// Attempted to access a field or index beyond the allocated slots for a gate.
pub const TRAP_OOB: u32 = 0x03;
/// Attempted a typed operation on a gate whose storage type does not match.
pub const TRAP_TYPE: u32 = 0x04;
/// The syscall ID provided is not recognized by the system.
pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007;
/// Not enough arguments on the stack for the requested syscall.
pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008;
/// Attempted to access a local slot that is out of bounds for the current frame.
pub const TRAP_INVALID_LOCAL: u32 = 0x0000_0009;
/// Division or modulo by zero.
pub const TRAP_DIV_ZERO: u32 = 0x0000_000A;
/// Attempted to call a function that does not exist in the function table.
pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
/// Executed RET with an incorrect stack height (mismatch with function metadata).
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
use serde::{Deserialize, Serialize};
/// Detailed information about a source code span.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SourceSpan {
pub file_id: u32,
pub start: u32,
pub end: u32,
}
/// Detailed information about a runtime trap.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrapInfo {
/// The specific trap code (e.g., TRAP_OOB).
pub code: u32,
/// The numeric value of the opcode that triggered the trap.
pub opcode: u16,
/// A human-readable message explaining the trap.
pub message: String,
/// The absolute Program Counter (PC) address where the trap occurred.
pub pc: u32,
/// Optional source span information if debug symbols are available.
pub span: Option<SourceSpan>,
}
/// Checks if an instruction is a jump (branch) instruction.
pub fn is_jump(opcode: OpCode) -> bool {
opcode.spec().is_branch
}
/// Checks if an instruction has any immediate operands in the instruction stream.
pub fn has_immediate(opcode: OpCode) -> bool {
opcode.spec().imm_bytes > 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trap_code_stability() {
// These numeric values are normative and must not change.
assert_eq!(TRAP_INVALID_GATE, 0x01);
assert_eq!(TRAP_DEAD_GATE, 0x02);
assert_eq!(TRAP_OOB, 0x03);
assert_eq!(TRAP_TYPE, 0x04);
assert_eq!(TRAP_INVALID_SYSCALL, 0x07);
assert_eq!(TRAP_STACK_UNDERFLOW, 0x08);
assert_eq!(TRAP_INVALID_LOCAL, 0x09);
assert_eq!(TRAP_DIV_ZERO, 0x0A);
assert_eq!(TRAP_INVALID_FUNC, 0x0B);
assert_eq!(TRAP_BAD_RET_SLOTS, 0x0C);
}
#[test]
fn test_abi_documentation_snapshot() {
// Snapshot of the ABI rules for traps and operands.
let abi_info = r#"
HIP Traps:
- INVALID_GATE (0x01): Non-existent gate handle.
- DEAD_GATE (0x02): Gate handle with RC=0.
- OOB (0x03): Access beyond allocated slots.
- TYPE (0x04): Type mismatch during heap access.
System Traps:
- INVALID_SYSCALL (0x07): Unknown syscall ID.
- STACK_UNDERFLOW (0x08): Missing syscall arguments.
- INVALID_LOCAL (0x09): Local slot out of bounds.
- DIV_ZERO (0x0A): Division by zero.
- INVALID_FUNC (0x0B): Function table index out of bounds.
- BAD_RET_SLOTS (0x0C): Stack height mismatch at RET.
Operand Sizes:
- Alloc: 8 bytes (u32 type_id, u32 slots)
- GateLoad: 4 bytes (u32 offset)
- GateStore: 4 bytes (u32 offset)
- PopN: 4 bytes (u32 count)
"#;
// This test serves as a "doc-lock".
// If you change the ABI, you must update this string.
let current_info = format!(
"\nHIP Traps:\n- INVALID_GATE (0x{:02X}): Non-existent gate handle.\n- DEAD_GATE (0x{:02X}): Gate handle with RC=0.\n- OOB (0x{:02X}): Access beyond allocated slots.\n- TYPE (0x{:02X}): Type mismatch during heap access.\n\nSystem Traps:\n- INVALID_SYSCALL (0x{:02X}): Unknown syscall ID.\n- STACK_UNDERFLOW (0x{:02X}): Missing syscall arguments.\n- INVALID_LOCAL (0x{:02X}): Local slot out of bounds.\n- DIV_ZERO (0x{:02X}): Division by zero.\n- INVALID_FUNC (0x{:02X}): Function table index out of bounds.\n- BAD_RET_SLOTS (0x{:02X}): Stack height mismatch at RET.\n\nOperand Sizes:\n- Alloc: {} bytes (u32 type_id, u32 slots)\n- GateLoad: {} bytes (u32 offset)\n- GateStore: {} bytes (u32 offset)\n- PopN: {} bytes (u32 count)\n",
TRAP_INVALID_GATE, TRAP_DEAD_GATE, TRAP_OOB, TRAP_TYPE,
TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW, TRAP_INVALID_LOCAL, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_BAD_RET_SLOTS,
operand_size(OpCode::Alloc),
operand_size(OpCode::GateLoad),
operand_size(OpCode::GateStore),
operand_size(OpCode::PopN)
);
assert_eq!(current_info.trim(), abi_info.trim());
}
#[test]
fn operand_size_matches_spec_for_all_opcodes() {
// Scan a generous numeric range and validate every decodable opcode.
// This ensures that abi::operand_size (if kept) never diverges from the
// canonical spec (OpcodeSpec::imm_bytes).
for raw in 0u16..=1023u16 {
if let Ok(op) = OpCode::try_from(raw) {
assert_eq!(
operand_size(op),
op.spec().imm_bytes as usize,
"operand_size must match spec for {:?}",
op
);
}
}
}
}

View File

@ -1,143 +0,0 @@
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),
/// A symbolic label that will be resolved to a PC address relative to another label.
RelLabel(String, 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 {
match operand {
Operand::U32(_) | Operand::I32(_) | Operand::Label(_) | Operand::RelLabel(_, _) => pcp += 4,
Operand::I64(_) | Operand::F64(_) => pcp += 8,
Operand::Bool(_) => pcp += 1,
}
}
pcp
}
pub struct AssembleResult {
pub code: Vec<u8>,
pub unresolved_labels: HashMap<String, Vec<u32>>,
}
/// 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 res = assemble_with_unresolved(instructions)?;
if !res.unresolved_labels.is_empty() {
let labels: Vec<_> = res.unresolved_labels.keys().cloned().collect();
return Err(format!("Undefined labels: {:?}", labels));
}
Ok(res.code)
}
pub fn assemble_with_unresolved(instructions: &[Asm]) -> Result<AssembleResult, String> {
let mut labels = HashMap::new();
let mut current_pc = 0u32;
// First pass: resolve labels
for instr in instructions {
match instr {
Asm::Label(name) => {
labels.insert(name.clone(), current_pc);
}
Asm::Op(_opcode, operands) => {
current_pc += 2; // OpCode is u16 (2 bytes)
current_pc = update_pc_by_operand(current_pc, operands);
}
}
}
// Second pass: generate bytes
let mut rom = Vec::new();
let mut unresolved_labels: HashMap<String, Vec<u32>> = HashMap::new();
let mut pc = 0u32;
for instr in instructions {
match instr {
Asm::Label(_) => {}
Asm::Op(opcode, operands) => {
write_u16_le(&mut rom, *opcode as u16).map_err(|e| e.to_string())?;
pc += 2;
for operand in operands {
match operand {
Operand::U32(v) => {
write_u32_le(&mut rom, *v).map_err(|e| e.to_string())?;
pc += 4;
}
Operand::I32(v) => {
write_u32_le(&mut rom, *v as u32).map_err(|e| e.to_string())?;
pc += 4;
}
Operand::I64(v) => {
write_i64_le(&mut rom, *v).map_err(|e| e.to_string())?;
pc += 8;
}
Operand::F64(v) => {
write_f64_le(&mut rom, *v).map_err(|e| e.to_string())?;
pc += 8;
}
Operand::Bool(v) => {
rom.push(if *v { 1 } else { 0 });
pc += 1;
}
Operand::Label(name) => {
if let Some(addr) = labels.get(name) {
write_u32_le(&mut rom, *addr).map_err(|e| e.to_string())?;
} else {
unresolved_labels.entry(name.clone()).or_default().push(pc);
write_u32_le(&mut rom, 0).map_err(|e| e.to_string())?; // Placeholder
}
pc += 4;
}
Operand::RelLabel(name, base) => {
let addr = labels.get(name).ok_or(format!("Undefined label: {}", name))?;
let base_addr = labels.get(base).ok_or(format!("Undefined base label: {}", base))?;
let rel_addr = (*addr as i64) - (*base_addr as i64);
write_u32_le(&mut rom, rel_addr as u32).map_err(|e| e.to_string())?;
pc += 4;
}
}
}
}
}
}
Ok(AssembleResult {
code: rom,
unresolved_labels,
})
}

View File

@ -1,70 +0,0 @@
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),
I32(i32),
I64(i64),
F64(f64),
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);
while (cursor.position() as usize) < rom.len() {
let pc = cursor.position() as u32;
let op_val = read_u16_le(&mut cursor).map_err(|e| e.to_string())?;
let opcode = OpCode::try_from(op_val)?;
let mut operands = Vec::new();
match opcode {
OpCode::PushConst | OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore | OpCode::Call => {
let v = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::U32(v));
}
OpCode::PushI64 | OpCode::PushF64 => {
let v = read_i64_le(&mut cursor).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::I64(v));
}
OpCode::PushBool => {
let mut b_buf = [0u8; 1];
cursor.read_exact(&mut b_buf).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::Bool(b_buf[0] != 0));
}
OpCode::Alloc => {
let v1 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
let v2 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::U32(v1));
operands.push(DisasmOperand::U32(v2));
}
_ => {}
}
instructions.push(Instr { pc, opcode, operands });
}
Ok(instructions)
}

View File

@ -1,11 +0,0 @@
pub fn read_u32_le(buf: &[u8], pos: usize) -> Option<u32> {
let b = buf.get(pos..pos + 4)?;
Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
pub fn write_u32_le(buf: &mut [u8], pos: usize, v: u32) -> Option<()> {
let b = buf.get_mut(pos..pos + 4)?;
let le = v.to_le_bytes();
b.copy_from_slice(&le);
Some(())
}

View File

@ -1,273 +0,0 @@
//! Shared bytecode layout utilities, used by both compiler (emitter/linker)
//! and the VM (verifier/loader). This ensures a single source of truth for
//! how function ranges, instruction boundaries, and pc→function lookups are
//! interpreted post-link.
use crate::decoder::decode_next;
use crate::FunctionMeta;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FunctionLayout {
pub start: usize,
pub end: usize, // exclusive
}
/// Precompute canonical [start, end) ranges for all functions.
///
/// Contract:
/// - Ranges are computed by sorting functions by `code_offset` (stable),
/// then using the next function's start as the current end; the last
/// function ends at `code_len_total`.
/// - The returned vector is indexed by the original function indices.
pub fn compute_function_layouts(functions: &[FunctionMeta], code_len_total: usize) -> Vec<FunctionLayout> {
// Build index array and sort by start offset (stable to preserve relative order).
let mut idxs: Vec<usize> = (0..functions.len()).collect();
idxs.sort_by_key(|&i| functions[i].code_offset as usize);
// Optional guard: offsets should be strictly increasing (duplicates are suspicious).
for w in idxs.windows(2) {
if let [a, b] = *w {
let sa = functions[a].code_offset as usize;
let sb = functions[b].code_offset as usize;
debug_assert!(sa < sb, "Function code_offset must be strictly increasing: {} vs {} (indices {} and {})", sa, sb, a, b);
}
}
let mut out = vec![FunctionLayout { start: 0, end: 0 }; functions.len()];
for (pos, &i) in idxs.iter().enumerate() {
let start = functions[i].code_offset as usize;
let end = if pos + 1 < idxs.len() {
functions[idxs[pos + 1]].code_offset as usize
} else {
code_len_total
};
out[i] = FunctionLayout { start, end };
}
out
}
/// Recomputes all `code_len` values in place from the next function start
/// (exclusive end), using the combined code buffer length for the last one.
pub fn recompute_function_lengths_in_place(functions: &mut [FunctionMeta], code_len_total: usize) {
let layouts = compute_function_layouts(functions, code_len_total);
for i in 0..functions.len() {
let start = layouts[i].start;
let end = layouts[i].end;
functions[i].code_len = end.saturating_sub(start) as u32;
}
}
/// Finds the function index that contains `pc_abs` (absolute), using the
/// canonical ranges (end = next start, exclusive). Returns `None` if none.
pub fn function_index_by_pc(functions: &[FunctionMeta], code_len_total: usize, pc_abs: usize) -> Option<usize> {
let layouts = compute_function_layouts(functions, code_len_total);
for i in 0..functions.len() {
let start = layouts[i].start;
let end = layouts[i].end;
if pc_abs >= start && pc_abs < end {
return Some(i);
}
}
None
}
/// Alias: canonical function lookup by absolute PC.
#[inline]
pub fn lookup_function_by_pc(functions: &[FunctionMeta], code_len_total: usize, pc_abs: usize) -> Option<usize> {
function_index_by_pc(functions, code_len_total, pc_abs)
}
/// Returns true if `rel_pc` (relative to the function start) is a valid
/// instruction boundary as determined by the canonical decoder.
///
/// Contract:
/// - `rel_pc == 0` is always a boundary if `func_idx` is valid.
/// - Boundaries are computed by stepping with `decoder::decode_next` from the
/// function start up to (and possibly past) `rel_pc` but never beyond the
/// function exclusive end.
/// - Any decode error before reaching `rel_pc` yields `false` (invalid program).
pub fn is_boundary(functions: &[FunctionMeta], code: &[u8], code_len_total: usize, func_idx: usize, rel_pc: usize) -> bool {
let (start, end) = match functions.get(func_idx) {
Some(_) => {
let layouts = compute_function_layouts(functions, code_len_total);
let l = &layouts[func_idx];
(l.start, l.end)
}
None => return false,
};
let func_len = end.saturating_sub(start);
if rel_pc == 0 { return true; }
if rel_pc > func_len { return false; }
let target = start + rel_pc;
let mut pc = start;
while pc < end {
match decode_next(pc, code) {
Ok(di) => {
let next = di.next_pc;
if next > end { return false; }
if next == target { return true; }
if next <= pc { return false; } // must make progress
pc = next;
if pc > target { return false; }
}
Err(_) => return false,
}
}
// If we reached end without matching `target`, only boundary is exact end
target == end
}
/// Returns true if `abs_pc` is a valid instruction boundary for the function
/// containing it, according to the canonical decoder. Returns false if `abs_pc`
/// is not within any function range or if decoding fails.
pub fn is_boundary_abs(functions: &[FunctionMeta], code: &[u8], code_len_total: usize, abs_pc: usize) -> bool {
if let Some(func_idx) = lookup_function_by_pc(functions, code_len_total, abs_pc) {
let layouts = compute_function_layouts(functions, code_len_total);
let (start, _end) = {
let l = &layouts[func_idx];
(l.start, l.end)
};
let rel = abs_pc.saturating_sub(start);
return is_boundary(functions, code, code_len_total, func_idx, rel);
}
// Not inside any function range; allow exact function starts/ends as
// valid boundaries (e.g., last function end == total code len).
let layouts = compute_function_layouts(functions, code_len_total);
for i in 0..functions.len() {
let start = layouts[i].start;
let end = layouts[i].end;
if abs_pc == start || abs_pc == end {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asm::{assemble, Asm, Operand};
use crate::opcode::OpCode;
fn build_funcs(offsets: &[usize], lens: Option<&[usize]>) -> Vec<FunctionMeta> {
let mut v = Vec::new();
for (i, off) in offsets.iter().copied().enumerate() {
let len_u32 = lens.and_then(|ls| ls.get(i).copied()).unwrap_or(0) as u32;
v.push(FunctionMeta {
code_offset: off as u32,
code_len: len_u32,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
});
}
v
}
#[test]
fn boundaries_known_sequence() {
// Build a function with mixed immediate sizes:
// [NOP][PUSH_I32 4][PUSH_I64 8][PUSH_BOOL 1][HALT]
let code = assemble(&[
Asm::Op(OpCode::Nop, vec![]),
Asm::Op(OpCode::PushI32, vec![Operand::I32(123)]),
Asm::Op(OpCode::PushI64, vec![Operand::I64(42)]),
Asm::Op(OpCode::PushBool, vec![Operand::Bool(true)]),
Asm::Op(OpCode::Halt, vec![]),
]).unwrap();
// Single function starting at 0
let code_len_total = code.len();
let mut funcs = build_funcs(&[0], None);
recompute_function_lengths_in_place(&mut funcs, code_len_total);
// Expected boundaries (relative): 0, 2, 8, 18, 21, 23
// Explanation per instruction size: opcode(2) + imm
let expected = [0usize, 2, 8, 18, 21, 23];
for rel in 0..=expected.last().copied().unwrap() {
let should_be_boundary = expected.contains(&rel);
assert_eq!(
is_boundary(&funcs, &code, code_len_total, 0, rel),
should_be_boundary,
"rel_pc={} boundary mismatch",
rel
);
}
// Check absolute variant too
for rel in expected {
let abs = rel;
assert!(is_boundary_abs(&funcs, &code, code_len_total, abs));
}
}
#[test]
fn fuzz_table_monotonic_and_boundaries() {
// Build a pseudo-random but valid sequence using a simple pattern over opcodes
// to avoid invalid encodings. We cycle through a few known-good opcodes.
let ops = [
OpCode::Nop,
OpCode::PushI32,
OpCode::PushBool,
OpCode::PushI64,
OpCode::Pop,
OpCode::Ret,
];
let mut prog = Vec::new();
for i in 0..50 {
let op = ops[i % ops.len()];
let asm = match op {
OpCode::Nop => Asm::Op(OpCode::Nop, vec![]),
OpCode::PushI32 => Asm::Op(OpCode::PushI32, vec![Operand::I32(i as i32)]),
OpCode::PushBool => Asm::Op(OpCode::PushBool, vec![Operand::Bool(i % 2 == 0)]),
OpCode::PushI64 => Asm::Op(OpCode::PushI64, vec![Operand::I64(i as i64)]),
OpCode::Pop => Asm::Op(OpCode::Pop, vec![]),
OpCode::Ret => Asm::Op(OpCode::Ret, vec![]),
_ => unreachable!(),
};
prog.push(asm);
}
let code = assemble(&prog).unwrap();
let code_len_total = code.len();
let mut funcs = build_funcs(&[0], None);
recompute_function_lengths_in_place(&mut funcs, code_len_total);
let layouts = compute_function_layouts(&funcs, code_len_total);
let (start, end) = (layouts[0].start, layouts[0].end);
assert_eq!(start, 0);
assert_eq!(end, code_len_total);
// Walk with decoder and verify boundaries are accepted
let mut pc = start;
while pc < end {
assert!(is_boundary_abs(&funcs, &code, code_len_total, pc));
let di = decode_next(pc, &code).expect("decode_next");
assert!(di.next_pc > pc && di.next_pc <= end);
pc = di.next_pc;
}
// End must be a boundary too
assert!(is_boundary(&funcs, &code, code_len_total, 0, end - start));
}
#[test]
fn compute_function_layouts_end_is_next_start() {
// Synthetic functions with known offsets: 0, 10, 25; total_len = 40
let funcs = build_funcs(&[0, 10, 25], None);
let layouts = compute_function_layouts(&funcs, 40);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0], FunctionLayout { start: 0, end: 10 });
assert_eq!(layouts[1], FunctionLayout { start: 10, end: 25 });
assert_eq!(layouts[2], FunctionLayout { start: 25, end: 40 });
for i in 0..3 {
let l = &layouts[i];
assert_eq!(l.end - l.start, (funcs.get(i + 1).map(|n| n.code_offset as usize).unwrap_or(40)) - (funcs[i].code_offset as usize));
}
}
}

View File

@ -1,16 +0,0 @@
pub mod opcode;
pub mod opcode_spec;
pub mod abi;
pub mod readwrite;
pub mod asm;
pub mod disasm;
pub mod layout;
pub mod decoder;
mod model;
pub mod io;
pub mod value;
pub mod program_image;
pub use model::*;
pub use value::Value;

View File

@ -1,52 +0,0 @@
//! Binary I/O utilities for the Prometeu ecosystem.
//! All multi-byte data is encoded in Little-Endian format.
use std::io::{self, Read, Write};
/// Reads a 16-bit unsigned integer from a reader.
pub fn read_u16_le<R: Read>(mut reader: R) -> io::Result<u16> {
let mut buf = [0u8; 2];
reader.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
/// Reads a 32-bit unsigned integer from a reader.
pub fn read_u32_le<R: Read>(mut reader: R) -> io::Result<u32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
/// Reads a 64-bit signed integer from a reader.
pub fn read_i64_le<R: Read>(mut reader: R) -> io::Result<i64> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(i64::from_le_bytes(buf))
}
/// Reads a 64-bit floating point from a reader.
pub fn read_f64_le<R: Read>(mut reader: R) -> io::Result<f64> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(f64::from_le_bytes(buf))
}
/// Writes a 16-bit unsigned integer to a writer.
pub fn write_u16_le<W: Write>(mut writer: W, val: u16) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
/// Writes a 32-bit unsigned integer to a writer.
pub fn write_u32_le<W: Write>(mut writer: W, val: u32) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
/// Writes a 64-bit signed integer to a writer.
pub fn write_i64_le<W: Write>(mut writer: W, val: i64) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}
/// Writes a 64-bit floating point to a writer.
pub fn write_f64_le<W: Write>(mut writer: W, val: f64) -> io::Result<()> {
writer.write_all(&val.to_le_bytes())
}

View File

@ -0,0 +1,50 @@
//! 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.
// --- HIP Trap Codes ---
/// Attempted to access a gate that does not exist or has been recycled incorrectly.
pub const TRAP_INVALID_GATE: u32 = 0x01;
/// Attempted to access a gate that has been explicitly released (RC=0).
pub const TRAP_DEAD_GATE: u32 = 0x02;
/// Attempted to access a field or index beyond the allocated slots for a gate.
pub const TRAP_OOB: u32 = 0x03;
/// Attempted a typed operation on a gate whose storage type does not match.
pub const TRAP_TYPE: u32 = 0x04;
/// The syscall ID provided is not recognized by the system.
pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007;
/// Not enough arguments on the stack for the requested syscall.
pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008;
/// Attempted to access a local slot that is out of bounds for the current frame.
pub const TRAP_INVALID_LOCAL: u32 = 0x0000_0009;
/// Division or modulo by zero.
pub const TRAP_DIV_ZERO: u32 = 0x0000_000A;
/// Attempted to call a function that does not exist in the function table.
pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
/// Executed RET with an incorrect stack height (mismatch with function metadata).
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
use serde::{Deserialize, Serialize};
/// Detailed information about a source code span.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SourceSpan {
pub file_id: u32,
pub start: u32,
pub end: u32,
}
/// Detailed information about a runtime trap.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrapInfo {
/// The specific trap code (e.g., TRAP_OOB).
pub code: u32,
/// The numeric value of the opcode that triggered the trap.
pub opcode: u16,
/// A human-readable message explaining the trap.
pub message: String,
/// The absolute Program Counter (PC) address where the trap occurred.
pub pc: u32,
/// Optional source span information if debug symbols are available.
pub span: Option<SourceSpan>,
}

View File

@ -90,7 +90,7 @@ impl<'a> DecodedInstr<'a> {
/// Decodes the instruction at program counter `pc` from `bytes`.
/// Returns the decoded instruction with canonical `next_pc`.
#[inline]
pub fn decode_next<'a>(pc: usize, bytes: &'a [u8]) -> Result<DecodedInstr<'a>, DecodeError> {
pub fn decode_next(pc: usize, bytes: &'_ [u8]) -> Result<DecodedInstr<'_>, DecodeError> {
if pc + 2 > bytes.len() {
return Err(DecodeError::TruncatedOpcode { pc });
}
@ -118,88 +118,4 @@ pub fn decode_next<'a>(pc: usize, bytes: &'a [u8]) -> Result<DecodedInstr<'a>, D
next_pc: imm_end,
imm: &bytes[imm_start..imm_end],
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asm::{assemble, Asm, Operand};
use crate::opcode::OpCode;
#[test]
fn decode_basic_no_imm() {
// Encode a NOP (0x0000)
let rom = assemble(&[Asm::Op(OpCode::Nop, vec![])]).unwrap();
let d = decode_next(0, &rom).unwrap();
assert_eq!(d.opcode, OpCode::Nop);
assert_eq!(d.pc, 0);
assert_eq!(d.next_pc, 2);
assert_eq!(d.imm.len(), 0);
}
#[test]
fn decode_with_u32_imm() {
// PUSH_CONST 0x11223344
let rom = assemble(&[Asm::Op(OpCode::PushConst, vec![Operand::U32(0x11223344)])]).unwrap();
let d = decode_next(0, &rom).unwrap();
assert_eq!(d.opcode, OpCode::PushConst);
assert_eq!(d.imm_u32().unwrap(), 0x11223344);
assert_eq!(d.next_pc, 2 + 4);
}
#[test]
fn decode_with_u8_imm() {
// PUSH_BOOL true
let rom = assemble(&[Asm::Op(OpCode::PushBool, vec![Operand::Bool(true)])]).unwrap();
let d = decode_next(0, &rom).unwrap();
assert_eq!(d.opcode, OpCode::PushBool);
assert_eq!(d.imm.len(), 1);
assert_eq!(d.imm_u8().unwrap(), 1);
assert_eq!(d.next_pc, 2 + 1);
}
#[test]
fn decode_with_i64_and_f64() {
// PUSH_I64, PUSH_F64
let rom = assemble(&[
Asm::Op(OpCode::PushI64, vec![Operand::I64(-123)]),
Asm::Op(OpCode::PushF64, vec![Operand::F64(3.25)]),
]).unwrap();
let d0 = decode_next(0, &rom).unwrap();
assert_eq!(d0.opcode, OpCode::PushI64);
assert_eq!(d0.imm_i64().unwrap(), -123);
let d1 = decode_next(d0.next_pc, &rom).unwrap();
assert_eq!(d1.opcode, OpCode::PushF64);
assert!((d1.imm_f64().unwrap() - 3.25).abs() < 1e-12);
}
#[test]
fn decode_truncated() {
let rom: Vec<u8> = vec![0x00, 0x00]; // NOP complete
assert!(matches!(decode_next(1, &rom), Err(DecodeError::TruncatedOpcode { .. })));
}
#[test]
fn roundtrip_encode_decode_table() {
let rom = assemble(&[
Asm::Op(OpCode::Nop, vec![]),
Asm::Op(OpCode::PushConst, vec![Operand::U32(7)]),
Asm::Op(OpCode::Jmp, vec![Operand::U32(4)]),
Asm::Op(OpCode::PushI64, vec![Operand::I64(42)]),
Asm::Op(OpCode::Halt, vec![]),
]).unwrap();
let mut pc = 0usize;
let mut decoded = Vec::new();
while pc < rom.len() {
let d = decode_next(pc, &rom).unwrap();
decoded.push(d.opcode);
pc = d.next_pc;
}
assert_eq!(decoded, vec![OpCode::Nop, OpCode::PushConst, OpCode::Jmp, OpCode::PushI64, OpCode::Halt]);
assert_eq!(pc, rom.len());
}
}
}

View File

@ -0,0 +1,84 @@
//! Shared bytecode layout utilities, used by both compiler (emitter/linker)
//! and the VM (verifier/loader). This ensures a single source of truth for
//! how function ranges, instruction boundaries, and pc→function lookups are
//! interpreted post-link.
use crate::model::FunctionMeta;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FunctionLayout {
pub start: usize,
pub end: usize, // exclusive
}
/// Precompute canonical [start, end) ranges for all functions.
///
/// Contract:
/// - Ranges are computed by sorting functions by `code_offset` (stable),
/// then using the next function's start as the current end; the last
/// function ends at `code_len_total`.
/// - The returned vector is indexed by the original function indices.
pub fn compute_function_layouts(functions: &[FunctionMeta], code_len_total: usize) -> Vec<FunctionLayout> {
// Build index array and sort by start offset (stable to preserve relative order).
let mut idxs: Vec<usize> = (0..functions.len()).collect();
idxs.sort_by_key(|&i| functions[i].code_offset as usize);
// Optional guard: offsets should be strictly increasing (duplicates are suspicious).
for w in idxs.windows(2) {
if let [a, b] = *w {
let sa = functions[a].code_offset as usize;
let sb = functions[b].code_offset as usize;
debug_assert!(sa < sb, "Function code_offset must be strictly increasing: {} vs {} (indices {} and {})", sa, sb, a, b);
}
}
let mut out = vec![FunctionLayout { start: 0, end: 0 }; functions.len()];
for (pos, &i) in idxs.iter().enumerate() {
let start = functions[i].code_offset as usize;
let end = if pos + 1 < idxs.len() {
functions[idxs[pos + 1]].code_offset as usize
} else {
code_len_total
};
out[i] = FunctionLayout { start, end };
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn build_funcs(offsets: &[usize], lens: Option<&[usize]>) -> Vec<FunctionMeta> {
let mut v = Vec::new();
for (i, off) in offsets.iter().copied().enumerate() {
let len_u32 = lens.and_then(|ls| ls.get(i).copied()).unwrap_or(0) as u32;
v.push(FunctionMeta {
code_offset: off as u32,
code_len: len_u32,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
});
}
v
}
#[test]
fn compute_function_layouts_end_is_next_start() {
// Synthetic functions with known offsets: 0, 10, 25; total_len = 40
let funcs = build_funcs(&[0, 10, 25], None);
let layouts = compute_function_layouts(&funcs, 40);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0], FunctionLayout { start: 0, end: 10 });
assert_eq!(layouts[1], FunctionLayout { start: 10, end: 25 });
assert_eq!(layouts[2], FunctionLayout { start: 25, end: 40 });
for i in 0..3 {
let l = &layouts[i];
assert_eq!(l.end - l.start, (funcs.get(i + 1).map(|n| n.code_offset as usize).unwrap_or(40)) - (funcs[i].code_offset as usize));
}
}
}

View File

@ -0,0 +1,33 @@
mod opcode;
mod opcode_spec;
mod abi;
mod layout;
mod decoder;
mod model;
mod value;
mod program_image;
pub use abi::{
TrapInfo,
TRAP_INVALID_LOCAL,
TRAP_OOB,
TRAP_TYPE,
TRAP_BAD_RET_SLOTS,
TRAP_DEAD_GATE,
TRAP_DIV_ZERO,
TRAP_INVALID_FUNC,
TRAP_INVALID_GATE,
TRAP_STACK_UNDERFLOW,
TRAP_INVALID_SYSCALL,
};
pub use model::{
BytecodeLoader,
FunctionMeta,
LoadError,
};
pub use value::Value;
pub use opcode_spec::OpCodeSpecExt;
pub use opcode::OpCode;
pub use decoder::{decode_next, DecodeError};
pub use layout::{compute_function_layouts, FunctionLayout};
pub use program_image::ProgramImage;

View File

@ -354,61 +354,3 @@ impl OpCode {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asm::{assemble, Asm, Operand};
#[test]
fn test_opcode_stability() {
// Normative test: ensures opcode numeric values are frozen.
assert_eq!(OpCode::Nop as u16, 0x00);
assert_eq!(OpCode::PushConst as u16, 0x10);
assert_eq!(OpCode::Alloc as u16, 0x60);
assert_eq!(OpCode::GateLoad as u16, 0x61);
assert_eq!(OpCode::GateStore as u16, 0x62);
assert_eq!(OpCode::GateBeginPeek as u16, 0x63);
assert_eq!(OpCode::GateEndPeek as u16, 0x64);
assert_eq!(OpCode::GateBeginBorrow as u16, 0x65);
assert_eq!(OpCode::GateEndBorrow as u16, 0x66);
assert_eq!(OpCode::GateBeginMutate as u16, 0x67);
assert_eq!(OpCode::GateEndMutate as u16, 0x68);
assert_eq!(OpCode::GateRetain as u16, 0x69);
assert_eq!(OpCode::GateRelease as u16, 0x6A);
assert_eq!(OpCode::FrameSync as u16, 0x80);
}
#[test]
fn test_hip_bytecode_golden() {
// Golden test for HIP opcodes and their encodings.
// Rule: All multi-byte operands are little-endian.
let instructions = vec![
Asm::Op(OpCode::Alloc, vec![Operand::U32(0x11223344), Operand::U32(0x55667788)]),
Asm::Op(OpCode::GateLoad, vec![Operand::U32(0xAABBCCDD)]),
Asm::Op(OpCode::GateStore, vec![Operand::U32(0x11223344)]),
Asm::Op(OpCode::GateBeginPeek, vec![]),
Asm::Op(OpCode::GateRetain, vec![]),
Asm::Op(OpCode::GateRelease, vec![]),
];
let bytes = assemble(&instructions).unwrap();
let mut expected = Vec::new();
// Alloc (0x60, 0x00) + type_id (44 33 22 11) + slots (88 77 66 55)
expected.extend_from_slice(&[0x60, 0x00, 0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55]);
// GateLoad (0x61, 0x00) + offset (DD CC BB AA)
expected.extend_from_slice(&[0x61, 0x00, 0xDD, 0xCC, 0xBB, 0xAA]);
// GateStore (0x62, 0x00) + offset (44 33 22 11)
expected.extend_from_slice(&[0x62, 0x00, 0x44, 0x33, 0x22, 0x11]);
// GateBeginPeek (0x63, 0x00)
expected.extend_from_slice(&[0x63, 0x00]);
// GateRetain (0x69, 0x00)
expected.extend_from_slice(&[0x69, 0x00]);
// GateRelease (0x6A, 0x00)
expected.extend_from_slice(&[0x6A, 0x00]);
assert_eq!(bytes, expected);
}
}

View File

@ -1,7 +1,7 @@
use crate::abi::TrapInfo;
use std::collections::HashMap;
use std::sync::Arc;
use crate::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
use crate::value::Value;
/// Represents a fully linked, executable PBS program image.

View File

@ -1,14 +0,0 @@
# PROMETEU Core
This crate contains the core library that defines the behavior of PROMETEU's hardware and software.
## Responsibilities
- **Logical Hardware**: Definition of GFX, Input, Audio, and Touch.
- **Virtual Machine**: Execution of custom bytecode (PBC - Prometeu Byte Code).
- **Virtual FS**: File management and "cartridge" access.
- **Firmware/OS**: The internal operating system that manages the application lifecycle, splash screens, and the Hub Home.
## Architecture
The core is designed to be deterministic and portable. It does not make direct calls to the host operating system; instead, it defines traits that hosts must implement (e.g., `FsBackend`).

View File

@ -5,6 +5,6 @@ edition = "2024"
license.workspace = true
[dependencies]
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
prometeu-bytecode = { path = "../prometeu-bytecode" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"

View File

@ -1,4 +1,4 @@
use prometeu_bytecode::Value;
use prometeu_bytecode::{Value, TRAP_OOB};
use crate::vm_fault::VmFault;
pub struct HostReturn<'a> {
@ -17,7 +17,7 @@ impl<'a> HostReturn<'a> {
}
pub fn push_bounded(&mut self, v: u32) -> Result<(), VmFault> {
if v > 0xFFFF {
return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_OOB, "Bounded value overflow".into()));
return Err(VmFault::Trap(TRAP_OOB, "Bounded value overflow".into()));
}
self.stack.push(Value::Bounded(v));
Ok(())

View File

@ -1,4 +1,4 @@
use prometeu_bytecode::Value;
use prometeu_bytecode::{Value, TRAP_TYPE};
use crate::vm_fault::VmFault;
pub fn expect_bounded(args: &[Value], idx: usize) -> Result<u32, VmFault> {
@ -7,13 +7,13 @@ pub fn expect_bounded(args: &[Value], idx: usize) -> Result<u32, VmFault> {
Value::Bounded(b) => Some(*b),
_ => None,
})
.ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Expected bounded at index {}", idx)))
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Expected bounded at index {}", idx)))
}
pub fn expect_int(args: &[Value], idx: usize) -> Result<i64, VmFault> {
args.get(idx)
.and_then(|v| v.as_integer())
.ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Expected integer at index {}", idx)))
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Expected integer at index {}", idx)))
}
pub fn expect_bool(args: &[Value], idx: usize) -> Result<bool, VmFault> {
@ -22,5 +22,5 @@ pub fn expect_bool(args: &[Value], idx: usize) -> Result<bool, VmFault> {
Value::Boolean(b) => Some(*b),
_ => None,
})
.ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Expected boolean at index {}", idx)))
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Expected boolean at index {}", idx)))
}

View File

@ -7,7 +7,7 @@ license.workspace = true
[dependencies]
serde_json = "1.0.149"
prometeu-vm = { path = "../prometeu-vm" }
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]

View File

@ -5,7 +5,7 @@ use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use std::collections::HashMap;
use std::time::Instant;
use prometeu_bytecode::Value;
use prometeu_bytecode::{Value, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE};
use prometeu_hal::{expect_bool, expect_int, HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_hal::asset::{BankType, LoadStatus, SlotRef};
use prometeu_hal::button::Button;
@ -352,7 +352,7 @@ impl VirtualMachineRuntime {
2 => LogLevel::Info,
3 => LogLevel::Warn,
4 => LogLevel::Error,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, format!("Invalid log level: {}", level_val))),
_ => return Err(VmFault::Trap(TRAP_TYPE, format!("Invalid log level: {}", level_val))),
};
let app_id = self.current_app_id;
@ -422,427 +422,6 @@ impl VirtualMachineRuntime {
}
}
#[cfg(test)]
mod tests {
use prometeu_bytecode::Value;
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::{HostReturn, InputSignals};
use super::*;
fn call_syscall(os: &mut VirtualMachineRuntime, id: u32, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Result<(), VmFault> {
let args_count = Syscall::from_u32(id).expect(&format!("Invalid syscall id: 0x{:08X}", id)).args_count();
let mut args = Vec::new();
for _ in 0..args_count {
// Protege contra underflow/erros de pilha durante testes
match vm.pop() {
Ok(v) => args.push(v),
Err(e) => return Err(VmFault::Panic(e)),
}
}
args.reverse();
let mut ret = HostReturn::new(&mut vm.operand_stack);
os.syscall(id, &args, &mut ret, &mut HostContext::new(Some(hw)))
}
#[test]
fn test_infinite_loop_budget_reset_bug() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
let rom = prometeu_bytecode::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: 6,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
}],
code: vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
debug_info: None,
exports: vec![prometeu_bytecode::Export { symbol: "main".into(), func_idx: 0 }],
}.serialize();
let cartridge = Cartridge {
app_id: 1234,
title: "test".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
entrypoint: "0".to_string(),
program: rom,
assets: vec![],
asset_table: vec![],
preload: vec![],
};
os.initialize_vm(&mut vm, &cartridge);
// First tick
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_1 = vm.cycles;
assert!(cycles_after_tick_1 >= VirtualMachineRuntime::CYCLES_PER_LOGICAL_FRAME);
// Second tick - Now it SHOULD NOT gain more budget
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_2 = vm.cycles;
// FIX: It should not have consumed cycles in the second tick because the logical frame budget ended
println!("Cycles after tick 1: {}, tick 2: {}", cycles_after_tick_1, cycles_after_tick_2);
assert_eq!(cycles_after_tick_2, cycles_after_tick_1, "VM should NOT have consumed more cycles in the second tick because logical frame budget is exhausted");
}
#[test]
fn test_budget_reset_on_frame_sync() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
// Loop that calls FrameSync:
// PUSH_CONST 0 (dummy)
// FrameSync (0x80)
// JMP 0
let rom = prometeu_bytecode::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: 8,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
}],
code: vec![
0x80, 0x00, // FrameSync (2 bytes opcode)
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // JMP 0 (2 bytes opcode + 4 bytes u32)
],
debug_info: None,
exports: vec![prometeu_bytecode::Export { symbol: "main".into(), func_idx: 0 }],
}.serialize();
let cartridge = Cartridge {
app_id: 1234,
title: "test".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
entrypoint: "0".to_string(),
program: rom,
assets: vec![],
asset_table: vec![],
preload: vec![],
};
os.initialize_vm(&mut vm, &cartridge);
// First tick
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_1 = vm.cycles;
// Should have stopped at FrameSync
assert!(cycles_after_tick_1 > 0, "VM should have consumed some cycles");
assert!(cycles_after_tick_1 < VirtualMachineRuntime::CYCLES_PER_LOGICAL_FRAME);
// Second tick - Should reset the budget and run a bit more until the next FrameSync
os.tick(&mut vm, &signals, &mut hw);
let cycles_after_tick_2 = vm.cycles;
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 = VirtualMachineRuntime::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, VirtualMachineRuntime::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 = VirtualMachineRuntime::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 = VirtualMachineRuntime::new(None);
// Deve retornar a cor raw diretamente
assert_eq!(os.get_color(0x07E0), Color::GREEN);
assert_eq!(os.get_color(0xF800), Color::RED);
assert_eq!(os.get_color(0x001F), Color::BLUE);
assert_eq!(os.get_color(3), Color::from_raw(3));
}
#[test]
fn test_gfx_set_sprite_syscall_pops() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
// Push arguments in order 1 to 10
vm.push(Value::String("mouse_cursor".to_string())); // arg1: assetName
vm.push(Value::Int32(0)); // arg2: id
// Simulating touch.x and touch.y syscalls
vm.push(Value::Int32(10)); // arg3: x (returned from syscall)
vm.push(Value::Int32(20)); // arg4: y (returned from syscall)
vm.push(Value::Int32(0)); // arg5: tileId
vm.push(Value::Int32(0)); // arg6: paletteId
vm.push(Value::Boolean(true)); // arg7: active
vm.push(Value::Boolean(false)); // arg8: flipX
vm.push(Value::Boolean(false)); // arg9: flipY
vm.push(Value::Int32(4)); // arg10: priority
let res = call_syscall(&mut os, 0x1007, &mut vm, &mut hw);
assert!(res.is_ok(), "GfxSetSprite syscall should succeed, but got: {:?}", res.err());
}
#[test]
fn test_gfx_set_sprite_with_swapped_arguments_repro() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
// Repro: what if the compiler is pushing in reverse order?
vm.push(Value::Int32(4)); // arg10?
vm.push(Value::Boolean(false));
vm.push(Value::Boolean(false));
vm.push(Value::Boolean(true));
vm.push(Value::Int32(0));
vm.push(Value::Int32(0));
vm.push(Value::Int32(20));
vm.push(Value::Int32(10));
vm.push(Value::Int32(0));
vm.push(Value::String("mouse_cursor".to_string())); // arg1?
let res = call_syscall(&mut os, 0x1007, &mut vm, &mut hw);
assert!(res.is_err());
// Because it tries to pop priority but gets a string
match res.err().unwrap() {
VmFault::Trap(code, _) => assert_eq!(code, prometeu_bytecode::abi::TRAP_TYPE),
_ => panic!("Expected Trap"),
}
}
#[test]
fn test_syscall_log_write_and_rate_limit() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
os.current_app_id = 123;
// 1. Normal log test
vm.push(Value::Int64(2)); // Info
vm.push(Value::String("Hello Log".to_string()));
let res = call_syscall(&mut os, 0x5001, &mut vm, &mut hw);
assert!(res.is_ok());
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg, "Hello Log");
assert_eq!(recent[0].level, LogLevel::Info);
assert_eq!(recent[0].source, LogSource::App { app_id: 123 });
// 2. Truncation test
let long_msg = "A".repeat(300);
vm.push(Value::Int64(3)); // Warn
vm.push(Value::String(long_msg));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg.len(), 256);
assert!(recent[0].msg.starts_with("AAAAA"));
// 3. Rate Limit Test
// We already made 2 logs. The limit is 10.
for i in 0..8 {
vm.push(Value::Int64(2));
vm.push(Value::String(format!("Log {}", i)));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
}
// The 11th log should be ignored (and generate a system warning)
vm.push(Value::Int64(2));
vm.push(Value::String("Eleventh log".to_string()));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(2);
// The last log should be the rate limit warning (came after the 10th log attempted)
assert_eq!(recent[1].msg, "App exceeded log limit per frame");
assert_eq!(recent[1].level, LogLevel::Warn);
// The log "Eleventh log" should not be there
assert_ne!(recent[0].msg, "Eleventh log");
// 4. Rate limit reset test in the next frame
os.begin_logical_frame(&InputSignals::default(), &mut hw);
vm.push(Value::Int64(2));
vm.push(Value::String("New frame log".to_string()));
call_syscall(&mut os, 0x5001, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg, "New frame log");
// 5. LOG_WRITE_TAG test
vm.push(Value::Int64(2)); // Info
vm.push(Value::Int64(42)); // Tag
vm.push(Value::String("Tagged Log".to_string()));
call_syscall(&mut os, 0x5002, &mut vm, &mut hw).unwrap();
let recent = os.log_service.get_recent(1);
assert_eq!(recent[0].msg, "Tagged Log");
assert_eq!(recent[0].tag, 42);
// Syscall de log é void: não empurra valor na pilha
// 6. GFX Syscall return test
vm.push(Value::Int64(1)); // color_idx
call_syscall(&mut os, 0x1001, &mut vm, &mut hw).unwrap(); // gfx.clear
assert_eq!(vm.pop().unwrap(), Value::Null);
}
#[test]
fn test_entrypoint_called_every_frame() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let signals = InputSignals::default();
// PushI32 0 (0x17), then Ret (0x51)
let rom = prometeu_bytecode::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: 10,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
}],
code: vec![
0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0
0x11, 0x00, // Pop
0x51, 0x00 // Ret
],
debug_info: None,
exports: vec![prometeu_bytecode::Export { symbol: "main".into(), func_idx: 0 }],
}.serialize();
let cartridge = Cartridge {
app_id: 1234,
title: "test".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
entrypoint: "0".to_string(),
program: rom,
assets: vec![],
asset_table: vec![],
preload: vec![],
};
os.initialize_vm(&mut vm, &cartridge);
// First frame
os.tick(&mut vm, &signals, &mut hw);
assert_eq!(os.logical_frame_index, 1);
assert!(!os.logical_frame_active);
assert!(vm.call_stack.is_empty());
// Second frame - Should call entrypoint again
os.tick(&mut vm, &signals, &mut hw);
assert_eq!(os.logical_frame_index, 2);
assert!(!os.logical_frame_active);
assert!(vm.call_stack.is_empty());
}
#[test]
fn test_os_unknown_syscall_returns_trap() {
let mut os = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hw = Hardware::new();
let mut ret = HostReturn::new(&mut vm.operand_stack);
let res = os.syscall(0xDEADBEEF, &[], &mut ret, &mut HostContext::new(Some(&mut hw)));
assert!(res.is_err());
match res.err().unwrap() {
VmFault::Trap(code, _) => assert_eq!(code, prometeu_bytecode::abi::TRAP_INVALID_SYSCALL),
_ => panic!("Expected Trap"),
}
}
#[test]
fn test_gfx_clear565_syscall() {
let mut hw = Hardware::new();
let mut os = VirtualMachineRuntime::new(None);
let mut stack = Vec::new();
// Success case
let args = vec![Value::Bounded(0xF800)]; // Red
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::GfxClear565 as u32, &args, &mut ret, &mut HostContext::new(Some(&mut hw))).unwrap();
}
assert_eq!(stack.len(), 0); // void return
// OOB case
let args = vec![Value::Bounded(0x10000)];
{
let mut ret = HostReturn::new(&mut stack);
let res = os.syscall(Syscall::GfxClear565 as u32, &args, &mut ret, &mut HostContext::new(Some(&mut hw)));
assert!(res.is_err());
match res.err().unwrap() {
VmFault::Trap(trap, _) => assert_eq!(trap, prometeu_bytecode::abi::TRAP_OOB),
_ => panic!("Expected Trap OOB"),
}
}
}
#[test]
fn test_input_snapshots_syscalls() {
let mut hw = Hardware::new();
let mut os = VirtualMachineRuntime::new(None);
// Pad snapshot
let mut stack = Vec::new();
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::InputPadSnapshot as u32, &[], &mut ret, &mut HostContext::new(Some(&mut hw))).unwrap();
}
assert_eq!(stack.len(), 48);
// Touch snapshot
let mut stack = Vec::new();
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::InputTouchSnapshot as u32, &[], &mut ret, &mut HostContext::new(Some(&mut hw))).unwrap();
}
assert_eq!(stack.len(), 6);
}
#[test]
fn test_os_syscall_without_hardware_fails_graciously() {
let mut os = VirtualMachineRuntime::new(None);
let mut stack = Vec::new();
let mut ret = HostReturn::new(&mut stack);
// GfxClear needs hardware
let res = os.syscall(Syscall::GfxClear as u32, &[Value::Int64(0)], &mut ret, &mut HostContext::new(None));
assert_eq!(res, Err(VmFault::Unavailable));
// SystemHasCart DOES NOT need hardware
let res = os.syscall(Syscall::SystemHasCart as u32, &[], &mut ret, &mut HostContext::new(None));
assert!(res.is_ok());
}
}
impl NativeInterface for VirtualMachineRuntime {
/// Dispatches a syscall from the VM to the native implementation.
///
@ -857,7 +436,7 @@ impl NativeInterface for VirtualMachineRuntime {
/// Each syscall returns the number of virtual cycles it consumed.
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext) -> Result<(), VmFault> {
self.telemetry_current.syscalls += 1;
let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(prometeu_bytecode::abi::TRAP_INVALID_SYSCALL, format!(
let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(TRAP_INVALID_SYSCALL, format!(
"Unknown syscall: 0x{:08X}", id
)))?;
@ -958,7 +537,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::GfxSetSprite => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
};
let index = expect_int(args, 1)? as usize;
let x = expect_int(args, 2)? as i32;
@ -971,7 +550,7 @@ impl NativeInterface for VirtualMachineRuntime {
let priority = expect_int(args, 9)? as u8;
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
if index < 512 {
*hw.gfx_mut().sprite_mut(index) = Sprite {
tile: Tile { id: tile_id, flip_x: false, flip_y: false, palette_id },
@ -992,7 +571,7 @@ impl NativeInterface for VirtualMachineRuntime {
let y = expect_int(args, 1)? as i32;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
let color_val = expect_int(args, 3)?;
let color = self.get_color(color_val);
@ -1004,7 +583,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF {
return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_OOB, "Color value out of bounds (bounded)".into()));
return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds (bounded)".into()));
}
let color = Color::from_raw(color_val as u16);
hw.gfx_mut().clear(color);
@ -1209,7 +788,7 @@ impl NativeInterface for VirtualMachineRuntime {
Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64,
Value::Bounded(b) => *b as f64,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected number for pitch".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected number for pitch".into())),
};
hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, prometeu_hal::LoopMode::Off);
@ -1221,7 +800,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::AudioPlay => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_name".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
};
let sample_id = expect_int(args, 1)? as u16;
let voice_id = expect_int(args, 2)? as usize;
@ -1232,7 +811,7 @@ impl NativeInterface for VirtualMachineRuntime {
Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64,
Value::Bounded(b) => *b as f64,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected number for pitch".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected number for pitch".into())),
};
let loop_mode = match expect_int(args, 6)? {
0 => prometeu_hal::LoopMode::Off,
@ -1252,7 +831,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::FsOpen => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())),
};
if self.fs_state != FsState::Mounted {
ret.push_int(-1);
@ -1282,7 +861,7 @@ impl NativeInterface for VirtualMachineRuntime {
let handle = expect_int(args, 0)? as u32;
let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? {
Value::String(s) => s.as_bytes().to_vec(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string content".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string content".into())),
};
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.write_file(path, &content) {
@ -1302,7 +881,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::FsListDir => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())),
};
match self.fs.list_dir(&path) {
Ok(entries) => {
@ -1317,7 +896,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::FsExists => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())),
};
ret.push_bool(self.fs.exists(&path));
Ok(())
@ -1326,7 +905,7 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::FsDelete => {
let path = match args.get(0).ok_or_else(|| VmFault::Panic("Missing path".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string path".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string path".into())),
};
match self.fs.delete(&path) {
Ok(_) => ret.push_bool(true),
@ -1342,7 +921,7 @@ impl NativeInterface for VirtualMachineRuntime {
let level = expect_int(args, 0)?;
let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
self.syscall_log_write(level, 0, msg)?;
// void
@ -1354,7 +933,7 @@ impl NativeInterface for VirtualMachineRuntime {
let tag = expect_int(args, 1)? as u16;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string message".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
self.syscall_log_write(level, tag, msg)?;
// void
@ -1365,15 +944,15 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::AssetLoad => {
let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? {
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Expected string asset_id".into())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_id".into())),
};
let asset_type_val = expect_int(args, 1)? as u32;
let slot_index = expect_int(args, 2)? as usize;
let asset_type = match asset_type_val {
0 => BankType::TILES,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let slot = SlotRef { asset_type, index: slot_index };
@ -1416,7 +995,7 @@ impl NativeInterface for VirtualMachineRuntime {
let asset_type = match asset_type_val {
0 => BankType::TILES,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let info = hw.assets().bank_info(asset_type);
let json = serde_json::to_string(&info).unwrap_or_default();
@ -1429,7 +1008,7 @@ impl NativeInterface for VirtualMachineRuntime {
let asset_type = match asset_type_val {
0 => BankType::TILES,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_TYPE, "Invalid asset type".to_string())),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let slot = SlotRef { asset_type, index: slot_index };
let info = hw.assets().slot_info(slot);

View File

@ -5,5 +5,5 @@ edition = "2024"
license.workspace = true
[dependencies]
prometeu-bytecode = { path = "../../compiler/prometeu-bytecode" }
prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-hal = { path = "../prometeu-hal" }

View File

@ -5,8 +5,6 @@ pub mod local_addressing;
pub mod verifier;
pub mod vm_init_error;
pub use prometeu_bytecode::abi::TrapInfo;
pub use prometeu_bytecode::opcode::OpCode;
pub use prometeu_hal::{
HostContext, HostReturn, NativeInterface, SyscallId,
};

View File

@ -1,5 +1,5 @@
use crate::call_frame::CallFrame;
use prometeu_bytecode::abi::{TrapInfo, TRAP_INVALID_LOCAL};
use prometeu_bytecode::{TrapInfo, TRAP_INVALID_LOCAL};
use prometeu_bytecode::FunctionMeta;
/// Computes the absolute stack index for the start of the current frame's locals (including args).

View File

@ -1,9 +1,9 @@
use prometeu_hal::syscalls::Syscall;
use prometeu_bytecode::decoder::{decode_next, DecodeError};
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::opcode_spec::OpCodeSpecExt;
use prometeu_bytecode::{decode_next, DecodeError};
use prometeu_bytecode::OpCode;
use prometeu_bytecode::OpCodeSpecExt;
use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::layout;
use prometeu_bytecode::{compute_function_layouts, FunctionLayout};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq, Eq)]
@ -28,7 +28,7 @@ impl Verifier {
pub fn verify(code: &[u8], functions: &[FunctionMeta]) -> Result<Vec<u16>, VerifierError> {
let mut max_stacks = Vec::with_capacity(functions.len());
// Precompute function [start, end) ranges once for O(1) lookups
let layouts = layout::compute_function_layouts(functions, code.len());
let layouts = compute_function_layouts(functions, code.len());
for (i, func) in functions.iter().enumerate() {
max_stacks.push(Self::verify_function(code, func, i, functions, &layouts)?);
}
@ -40,7 +40,7 @@ impl Verifier {
func: &FunctionMeta,
func_idx: usize,
all_functions: &[FunctionMeta],
layouts: &[layout::FunctionLayout],
layouts: &[FunctionLayout],
) -> Result<u16, VerifierError> {
let func_start = func.code_offset as usize;
// Use precomputed canonical range end

View File

@ -1,16 +1,13 @@
use crate::call_frame::CallFrame;
use crate::scope_frame::ScopeFrame;
use crate::{HostContext, NativeInterface};
use prometeu_bytecode::abi::{
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE, TRAP_OOB,
TRAP_TYPE,
};
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::program_image::ProgramImage;
use prometeu_bytecode::Value;
use prometeu_hal::vm_fault::VmFault;
use crate::verifier::Verifier;
use crate::vm_init_error::VmInitError;
use crate::{HostContext, NativeInterface};
use prometeu_bytecode::{TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE};
use prometeu_bytecode::OpCode;
use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value;
use prometeu_hal::vm_fault::VmFault;
/// Reason why the Virtual Machine stopped execution during a specific run.
/// This allows the system to decide if it should continue execution in the next tick
@ -363,7 +360,7 @@ impl VirtualMachine {
let start_pc = self.pc;
// Fetch & Decode
let instr = prometeu_bytecode::decoder::decode_next(self.pc, &self.program.rom)
let instr = prometeu_bytecode::decode_next(self.pc, &self.program.rom)
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let opcode = instr.opcode;
@ -884,7 +881,7 @@ impl VirtualMachine {
let pc_at_syscall = start_pc as u32;
let id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let syscall = prometeu_hal::syscalls::Syscall::from_u32(id).ok_or_else(|| {
self.trap(prometeu_bytecode::abi::TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!("Unknown syscall: 0x{:08X}", id), pc_at_syscall)
self.trap(TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!("Unknown syscall: 0x{:08X}", id), pc_at_syscall)
})?;
let args_count = syscall.args_count();
@ -892,7 +889,7 @@ impl VirtualMachine {
let mut args = Vec::with_capacity(args_count);
for _ in 0..args_count {
let v = self.pop().map_err(|_e| {
self.trap(prometeu_bytecode::abi::TRAP_STACK_UNDERFLOW, OpCode::Syscall as u16, "Syscall argument stack underflow".to_string(), pc_at_syscall)
self.trap(TRAP_STACK_UNDERFLOW, OpCode::Syscall as u16, "Syscall argument stack underflow".to_string(), pc_at_syscall)
})?;
args.push(v);
}
@ -1007,9 +1004,8 @@ mod tests {
}]);
vm
}
use crate::{HostReturn};
use prometeu_bytecode::abi::SourceSpan;
use prometeu_bytecode::FunctionMeta;
use crate::HostReturn;
use prometeu_bytecode::{FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW};
use prometeu_hal::expect_int;
struct MockNative;
@ -1669,7 +1665,7 @@ mod tests {
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_OOB);
assert_eq!(trap.code, TRAP_OOB);
assert_eq!(trap.opcode, OpCode::GateLoad as u16);
assert!(trap.message.contains("Out-of-bounds"));
}
@ -1696,7 +1692,7 @@ mod tests {
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_TYPE);
assert_eq!(trap.code, TRAP_TYPE);
assert_eq!(trap.opcode, OpCode::GateLoad as u16);
}
_ => panic!("Expected Trap, got {:?}", report.reason),
@ -1772,7 +1768,7 @@ mod tests {
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_INVALID_GATE);
assert_eq!(trap.code, TRAP_INVALID_GATE);
assert_eq!(trap.opcode, OpCode::GateLoad as u16);
}
_ => panic!("Expected Trap, got {:?}", report.reason),
@ -1903,7 +1899,7 @@ mod tests {
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_TYPE);
assert_eq!(trap.code, TRAP_TYPE);
assert_eq!(trap.opcode, OpCode::Syscall as u16);
}
_ => panic!("Expected Trap, got {:?}", report.reason),
@ -1925,7 +1921,7 @@ mod tests {
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_INVALID_SYSCALL);
assert_eq!(trap.code, TRAP_INVALID_SYSCALL);
assert_eq!(trap.opcode, OpCode::Syscall as u16);
assert!(trap.message.contains("Unknown syscall"));
assert_eq!(trap.pc, 0);
@ -1950,7 +1946,7 @@ mod tests {
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_STACK_UNDERFLOW);
assert_eq!(trap.code, TRAP_STACK_UNDERFLOW);
assert_eq!(trap.opcode, OpCode::Syscall as u16);
assert!(trap.message.contains("underflow"));
assert_eq!(trap.pc, 0);
@ -1998,7 +1994,7 @@ mod tests {
assert!(res.is_err());
match res.err().unwrap() {
VmFault::Trap(code, _) => {
assert_eq!(code, prometeu_bytecode::abi::TRAP_OOB);
assert_eq!(code, TRAP_OOB);
}
_ => panic!("Expected Trap"),
}
@ -2430,7 +2426,7 @@ mod tests {
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_INVALID_LOCAL);
assert_eq!(trap.code, TRAP_INVALID_LOCAL);
assert_eq!(trap.opcode, OpCode::GetLocal as u16);
assert!(trap.message.contains("out of bounds"));
}
@ -2577,97 +2573,4 @@ mod tests {
_ => panic!("Expected Trap, got {:?}", report.reason),
}
}
#[test]
fn test_traceable_trap_with_span() {
let mut rom = Vec::new();
// 0: PUSH_I32 10 (6 bytes)
// 6: PUSH_I32 0 (6 bytes)
// 12: DIV (2 bytes)
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&10i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&0i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes());
let mut pc_to_span = Vec::new();
pc_to_span.push((0, SourceSpan { file_id: 1, start: 10, end: 15 }));
pc_to_span.push((6, SourceSpan { file_id: 1, start: 16, end: 20 }));
pc_to_span.push((12, SourceSpan { file_id: 1, start: 21, end: 25 }));
let debug_info = prometeu_bytecode::DebugInfo {
pc_to_span,
function_names: vec![(0, "main".to_string())],
};
let program = ProgramImage::new(rom.clone(), vec![], vec![FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
..Default::default()
}], Some(debug_info), std::collections::HashMap::new());
let mut vm = VirtualMachine {
program,
..Default::default()
};
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, TRAP_DIV_ZERO);
assert_eq!(trap.pc, 12);
assert_eq!(trap.span, Some(SourceSpan { file_id: 1, start: 21, end: 25 }));
}
_ => panic!("Expected Trap, got {:?}", report.reason),
}
}
#[test]
fn test_traceable_trap_with_function_name() {
let mut rom = Vec::new();
// 0: PUSH_I32 10 (6 bytes)
// 6: PUSH_I32 0 (6 bytes)
// 12: DIV (2 bytes)
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&10i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&0i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Div as u16).to_le_bytes());
let pc_to_span = vec![(12, SourceSpan { file_id: 1, start: 21, end: 25 })];
let function_names = vec![(0, "math_utils::divide".to_string())];
let debug_info = prometeu_bytecode::DebugInfo {
pc_to_span,
function_names,
};
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
..Default::default()
}];
let program = ProgramImage::new(rom, vec![], functions, Some(debug_info), std::collections::HashMap::new());
let mut vm = VirtualMachine {
program,
..Default::default()
};
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
match report.reason {
LogicalFrameEndingReason::Trap(trap) => {
assert_eq!(trap.code, TRAP_DIV_ZERO);
assert!(trap.message.contains("math_utils::divide"));
}
_ => panic!("Expected Trap, got {:?}", report.reason),
}
}
}

View File

@ -1,18 +0,0 @@
# PROMETEU Desktop Runtime
This is the host implementation for desktop platforms, allowing PROMETEU to run on Windows, Linux, and macOS.
## Features
- **Rendering**: Uses the GPU via the `pixels` library to present the low-resolution framebuffer.
- **Input**: Translates keyboard and controller events (via `winit`) into PROMETEU hardware signals.
- **Audio**: Interfaces with the host's sound system (via `cpal`).
- **Debugging**: Hosts a TCP server that implements the **DevTools Protocol**, allowing connections from external IDEs or debuggers.
## How to run
Generally executed via the main CLI (`prometeu`), but can be called directly for testing:
```bash
cargo run -- --run path/to/cart
```

View File

@ -1,16 +0,0 @@
# PROMETEU CLI (Dispatcher)
The `prometeu` binary acts as the unified front-end for the ecosystem. It does not implement the execution or compilation logic but knows where to find the binaries that do.
## Commands
- `prometeu run <cart>`: Runs a cartridge using the available runtime.
- `prometeu debug <cart> [--port <p>]`: Starts execution in debug mode.
- `prometeu build <projectDir>`: Calls the `prometeuc` compiler.
- `prometeu verify c <projectDir>`: Calls the `prometeuc` compiler.
- `prometeu pack <cartDir>`: (Planned) Calls the `prometeup` packager.
- `prometeu verify p <cartDir>`: (Planned) Calls the `prometeup` packager.
## How it works
The dispatcher locates sibling binaries (`prometeu-host-desktop-winit`, `prometeuc`, etc.) in the same directory where it is installed. It inherits `stdin`, `stdout`, and `stderr`, and propagates the exit code of the called process.