realign and clean up
This commit is contained in:
parent
90ecd77031
commit
f75c61004c
@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/compiler/prometeu-bytecode",
|
||||
"crates/console/prometeu-bytecode",
|
||||
|
||||
"crates/console/prometeu-drivers",
|
||||
"crates/console/prometeu-firmware",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
```
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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())
|
||||
}
|
||||
50
crates/console/prometeu-bytecode/src/abi.rs
Normal file
50
crates/console/prometeu-bytecode/src/abi.rs
Normal 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>,
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
84
crates/console/prometeu-bytecode/src/layout.rs
Normal file
84
crates/console/prometeu-bytecode/src/layout.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
crates/console/prometeu-bytecode/src/lib.rs
Normal file
33
crates/console/prometeu-bytecode/src/lib.rs
Normal 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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
@ -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`).
|
||||
@ -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"
|
||||
@ -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(())
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
```
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user