quality baseline

This commit is contained in:
bQUARKz 2026-02-18 07:31:29 +00:00
parent e2a970e69c
commit c7786fa8b0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
90 changed files with 3076 additions and 1334 deletions

29
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: CI
on:
push:
branches: [ main, master ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Format check
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy --workspace --all-features
- name: Test
run: cargo test --workspace --all-targets --all-features --no-fail-fast

68
Cargo.lock generated
View File

@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.3.4",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy",
@ -669,6 +669,17 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@ -879,7 +890,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.4",
"libc", "libc",
] ]
@ -1545,6 +1556,15 @@ dependencies = [
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "presser" name = "presser"
version = "0.3.1" version = "0.3.1"
@ -1644,12 +1664,20 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "prometeu-test-support"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "prometeu-vm" name = "prometeu-vm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"prometeu-bytecode", "prometeu-bytecode",
"prometeu-hal", "prometeu-hal",
"prometeu-test-support",
] ]
[[package]] [[package]]
@ -1676,6 +1704,36 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]] [[package]]
name = "range-alloc" name = "range-alloc"
version = "0.1.4" version = "0.1.4"
@ -2162,6 +2220,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.1+wasi-0.2.4" version = "1.0.1+wasi-0.2.4"

View File

@ -11,6 +11,7 @@ members = [
"crates/host/prometeu-host-desktop-winit", "crates/host/prometeu-host-desktop-winit",
"crates/tools/prometeu-cli", "crates/tools/prometeu-cli",
"crates/dev/prometeu-test-support",
] ]
resolver = "2" resolver = "2"

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
.PHONY: fmt fmt-check clippy test ci
fmt:
cargo fmt
fmt-check:
cargo fmt -- --check
clippy:
cargo clippy --workspace --all-features
test:
cargo test --workspace --all-targets --all-features --no-fail-fast
ci: fmt-check clippy test

View File

@ -112,10 +112,5 @@ pub fn decode_next(pc: usize, bytes: &'_ [u8]) -> Result<DecodedInstr<'_>, Decod
}); });
} }
Ok(DecodedInstr { Ok(DecodedInstr { opcode, pc, next_pc: imm_end, imm: &bytes[imm_start..imm_end] })
opcode,
pc,
next_pc: imm_end,
imm: &bytes[imm_start..imm_end],
})
} }

View File

@ -18,7 +18,10 @@ pub struct FunctionLayout {
/// then using the next function's start as the current end; the last /// then using the next function's start as the current end; the last
/// function ends at `code_len_total`. /// function ends at `code_len_total`.
/// - The returned vector is indexed by the original function indices. /// - The returned vector is indexed by the original function indices.
pub fn compute_function_layouts(functions: &[FunctionMeta], code_len_total: usize) -> Vec<FunctionLayout> { 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). // Build index array and sort by start offset (stable to preserve relative order).
let mut idxs: Vec<usize> = (0..functions.len()).collect(); let mut idxs: Vec<usize> = (0..functions.len()).collect();
idxs.sort_by_key(|&i| functions[i].code_offset as usize); idxs.sort_by_key(|&i| functions[i].code_offset as usize);
@ -28,7 +31,14 @@ pub fn compute_function_layouts(functions: &[FunctionMeta], code_len_total: usiz
if let [a, b] = *w { if let [a, b] = *w {
let sa = functions[a].code_offset as usize; let sa = functions[a].code_offset as usize;
let sb = functions[b].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); debug_assert!(
sa < sb,
"Function code_offset must be strictly increasing: {} vs {} (indices {} and {})",
sa,
sb,
a,
b
);
} }
} }
@ -78,7 +88,11 @@ mod tests {
for i in 0..3 { for i in 0..3 {
let l = &layouts[i]; 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)); assert_eq!(
l.end - l.start,
(funcs.get(i + 1).map(|n| n.code_offset as usize).unwrap_or(40))
- (funcs[i].code_offset as usize)
);
} }
} }
} }

View File

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

View File

@ -81,15 +81,26 @@ impl BytecodeModule {
let cp_data = self.serialize_const_pool(); let cp_data = self.serialize_const_pool();
let func_data = self.serialize_functions(); let func_data = self.serialize_functions();
let code_data = self.code.clone(); let code_data = self.code.clone();
let debug_data = self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default(); let debug_data =
self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
let export_data = self.serialize_exports(); let export_data = self.serialize_exports();
let mut final_sections = Vec::new(); let mut final_sections = Vec::new();
if !cp_data.is_empty() { final_sections.push((0, cp_data)); } if !cp_data.is_empty() {
if !func_data.is_empty() { final_sections.push((1, func_data)); } final_sections.push((0, cp_data));
if !code_data.is_empty() { final_sections.push((2, code_data)); } }
if !debug_data.is_empty() { final_sections.push((3, debug_data)); } if !func_data.is_empty() {
if !export_data.is_empty() { final_sections.push((4, export_data)); } final_sections.push((1, func_data));
}
if !code_data.is_empty() {
final_sections.push((2, code_data));
}
if !debug_data.is_empty() {
final_sections.push((3, debug_data));
}
if !export_data.is_empty() {
final_sections.push((4, export_data));
}
let mut out = Vec::new(); let mut out = Vec::new();
// Magic "PBS\0" // Magic "PBS\0"
@ -123,7 +134,9 @@ impl BytecodeModule {
} }
fn serialize_const_pool(&self) -> Vec<u8> { fn serialize_const_pool(&self) -> Vec<u8> {
if self.const_pool.is_empty() { return Vec::new(); } if self.const_pool.is_empty() {
return Vec::new();
}
let mut data = Vec::new(); let mut data = Vec::new();
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes()); data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
for entry in &self.const_pool { for entry in &self.const_pool {
@ -157,7 +170,9 @@ impl BytecodeModule {
} }
fn serialize_functions(&self) -> Vec<u8> { fn serialize_functions(&self) -> Vec<u8> {
if self.functions.is_empty() { return Vec::new(); } if self.functions.is_empty() {
return Vec::new();
}
let mut data = Vec::new(); let mut data = Vec::new();
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes()); data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
for f in &self.functions { for f in &self.functions {
@ -191,7 +206,9 @@ impl BytecodeModule {
} }
fn serialize_exports(&self) -> Vec<u8> { fn serialize_exports(&self) -> Vec<u8> {
if self.exports.is_empty() { return Vec::new(); } if self.exports.is_empty() {
return Vec::new();
}
let mut data = Vec::new(); let mut data = Vec::new();
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes()); data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
for exp in &self.exports { for exp in &self.exports {
@ -202,7 +219,6 @@ impl BytecodeModule {
} }
data data
} }
} }
pub struct BytecodeLoader; pub struct BytecodeLoader;
@ -224,7 +240,8 @@ impl BytecodeLoader {
} }
let endianness = bytes[6]; let endianness = bytes[6];
if endianness != 0 { // 0 = Little Endian if endianness != 0 {
// 0 = Little Endian
return Err(LoadError::InvalidEndianness); return Err(LoadError::InvalidEndianness);
} }
@ -236,9 +253,20 @@ impl BytecodeLoader {
if pos + 12 > bytes.len() { if pos + 12 > bytes.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let kind = u32::from_le_bytes([bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3]]); let kind =
let offset = u32::from_le_bytes([bytes[pos+4], bytes[pos+5], bytes[pos+6], bytes[pos+7]]); u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
let length = u32::from_le_bytes([bytes[pos+8], bytes[pos+9], bytes[pos+10], bytes[pos+11]]); let offset = u32::from_le_bytes([
bytes[pos + 4],
bytes[pos + 5],
bytes[pos + 6],
bytes[pos + 7],
]);
let length = u32::from_le_bytes([
bytes[pos + 8],
bytes[pos + 9],
bytes[pos + 10],
bytes[pos + 11],
]);
// Basic bounds check // Basic bounds check
if (offset as usize) + (length as usize) > bytes.len() { if (offset as usize) + (length as usize) > bytes.len() {
@ -273,19 +301,24 @@ impl BytecodeLoader {
for (kind, offset, length) in sections { for (kind, offset, length) in sections {
let section_data = &bytes[offset as usize..(offset + length) as usize]; let section_data = &bytes[offset as usize..(offset + length) as usize];
match kind { match kind {
0 => { // Const Pool 0 => {
// Const Pool
module.const_pool = parse_const_pool(section_data)?; module.const_pool = parse_const_pool(section_data)?;
} }
1 => { // Functions 1 => {
// Functions
module.functions = parse_functions(section_data)?; module.functions = parse_functions(section_data)?;
} }
2 => { // Code 2 => {
// Code
module.code = section_data.to_vec(); module.code = section_data.to_vec();
} }
3 => { // Debug Info 3 => {
// Debug Info
module.debug_info = Some(parse_debug_section(section_data)?); module.debug_info = Some(parse_debug_section(section_data)?);
} }
4 => { // Exports 4 => {
// Exports
module.exports = parse_exports(section_data)?; module.exports = parse_exports(section_data)?;
} }
_ => {} // Skip unknown or optional sections _ => {} // Skip unknown or optional sections
@ -318,35 +351,52 @@ fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
pos += 1; pos += 1;
match tag { match tag {
0 => cp.push(ConstantPoolEntry::Null), 0 => cp.push(ConstantPoolEntry::Null),
1 => { // Int64 1 => {
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); } // Int64
let val = i64::from_le_bytes(data[pos..pos+8].try_into().unwrap()); if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
cp.push(ConstantPoolEntry::Int64(val)); cp.push(ConstantPoolEntry::Int64(val));
pos += 8; pos += 8;
} }
2 => { // Float64 2 => {
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); } // Float64
let val = f64::from_le_bytes(data[pos..pos+8].try_into().unwrap()); if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
cp.push(ConstantPoolEntry::Float64(val)); cp.push(ConstantPoolEntry::Float64(val));
pos += 8; pos += 8;
} }
3 => { // Boolean 3 => {
if pos >= data.len() { return Err(LoadError::UnexpectedEof); } // Boolean
if pos >= data.len() {
return Err(LoadError::UnexpectedEof);
}
cp.push(ConstantPoolEntry::Boolean(data[pos] != 0)); cp.push(ConstantPoolEntry::Boolean(data[pos] != 0));
pos += 1; pos += 1;
} }
4 => { // String 4 => {
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); } // String
let len = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize; if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4; pos += 4;
if pos + len > data.len() { return Err(LoadError::UnexpectedEof); } if pos + len > data.len() {
let s = String::from_utf8_lossy(&data[pos..pos+len]).into_owned(); return Err(LoadError::UnexpectedEof);
}
let s = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
cp.push(ConstantPoolEntry::String(s)); cp.push(ConstantPoolEntry::String(s));
pos += len; pos += len;
} }
5 => { // Int32 5 => {
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); } // Int32
let val = i32::from_le_bytes(data[pos..pos+4].try_into().unwrap()); if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = i32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
cp.push(ConstantPoolEntry::Int32(val)); cp.push(ConstantPoolEntry::Int32(val));
pos += 4; pos += 4;
} }
@ -371,12 +421,12 @@ fn parse_functions(data: &[u8]) -> Result<Vec<FunctionMeta>, LoadError> {
if pos + 16 > data.len() { if pos + 16 > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let code_offset = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()); let code_offset = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let code_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()); let code_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
let param_slots = u16::from_le_bytes(data[pos+8..pos+10].try_into().unwrap()); let param_slots = u16::from_le_bytes(data[pos + 8..pos + 10].try_into().unwrap());
let local_slots = u16::from_le_bytes(data[pos+10..pos+12].try_into().unwrap()); let local_slots = u16::from_le_bytes(data[pos + 10..pos + 12].try_into().unwrap());
let return_slots = u16::from_le_bytes(data[pos+12..pos+14].try_into().unwrap()); let return_slots = u16::from_le_bytes(data[pos + 12..pos + 14].try_into().unwrap());
let max_stack_slots = u16::from_le_bytes(data[pos+14..pos+16].try_into().unwrap()); let max_stack_slots = u16::from_le_bytes(data[pos + 14..pos + 16].try_into().unwrap());
functions.push(FunctionMeta { functions.push(FunctionMeta {
code_offset, code_offset,
@ -402,17 +452,17 @@ fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
let mut pos = 0; let mut pos = 0;
// PC to Span table // PC to Span table
let span_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize; let span_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4; pos += 4;
let mut pc_to_span = Vec::with_capacity(span_count); let mut pc_to_span = Vec::with_capacity(span_count);
for _ in 0..span_count { for _ in 0..span_count {
if pos + 16 > data.len() { if pos + 16 > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let pc = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()); let pc = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let file_id = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()); let file_id = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
let start = u32::from_le_bytes(data[pos+8..pos+12].try_into().unwrap()); let start = u32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
let end = u32::from_le_bytes(data[pos+12..pos+16].try_into().unwrap()); let end = u32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
pc_to_span.push((pc, SourceSpan { file_id, start, end })); pc_to_span.push((pc, SourceSpan { file_id, start, end }));
pos += 16; pos += 16;
} }
@ -421,20 +471,20 @@ fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
if pos + 4 > data.len() { if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let func_name_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize; let func_name_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4; pos += 4;
let mut function_names = Vec::with_capacity(func_name_count); let mut function_names = Vec::with_capacity(func_name_count);
for _ in 0..func_name_count { for _ in 0..func_name_count {
if pos + 8 > data.len() { if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()); let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize; let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
pos += 8; pos += 8;
if pos + name_len > data.len() { if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let name = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned(); let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
function_names.push((func_idx, name)); function_names.push((func_idx, name));
pos += name_len; pos += name_len;
} }
@ -457,20 +507,19 @@ fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
if pos + 8 > data.len() { if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()); let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize; let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
pos += 8; pos += 8;
if pos + name_len > data.len() { if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof); return Err(LoadError::UnexpectedEof);
} }
let symbol = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned(); let symbol = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
exports.push(Export { symbol, func_idx }); exports.push(Export { symbol, func_idx });
pos += name_len; pos += name_len;
} }
Ok(exports) Ok(exports)
} }
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> { fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
for func in &module.functions { for func in &module.functions {
// Opcode stream bounds // Opcode stream bounds
@ -485,22 +534,35 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
if pos + 2 > module.code.len() { if pos + 2 > module.code.len() {
break; // Unexpected EOF in middle of opcode, maybe should be error break; // Unexpected EOF in middle of opcode, maybe should be error
} }
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos+1]]); let op_val = u16::from_le_bytes([module.code[pos], module.code[pos + 1]]);
let opcode = OpCode::try_from(op_val).map_err(|_| LoadError::InvalidOpcode)?; let opcode = OpCode::try_from(op_val).map_err(|_| LoadError::InvalidOpcode)?;
pos += 2; pos += 2;
match opcode { match opcode {
OpCode::PushConst => { OpCode::PushConst => {
if pos + 4 > module.code.len() { return Err(LoadError::UnexpectedEof); } if pos + 4 > module.code.len() {
let idx = u32::from_le_bytes(module.code[pos..pos+4].try_into().unwrap()) as usize; return Err(LoadError::UnexpectedEof);
}
let idx =
u32::from_le_bytes(module.code[pos..pos + 4].try_into().unwrap()) as usize;
if idx >= module.const_pool.len() { if idx >= module.const_pool.len() {
return Err(LoadError::InvalidConstIndex); return Err(LoadError::InvalidConstIndex);
} }
pos += 4; pos += 4;
} }
OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue OpCode::PushI32
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal | OpCode::PushBounded
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => { | OpCode::Jmp
| OpCode::JmpIfFalse
| OpCode::JmpIfTrue
| OpCode::GetGlobal
| OpCode::SetGlobal
| OpCode::GetLocal
| OpCode::SetLocal
| OpCode::PopN
| OpCode::Syscall
| OpCode::GateLoad
| OpCode::GateStore => {
pos += 4; pos += 4;
} }
OpCode::PushI64 | OpCode::PushF64 => { OpCode::PushI64 | OpCode::PushF64 => {
@ -604,10 +666,10 @@ mod tests {
// Setup functions section // Setup functions section
let func_data_start = 64; let func_data_start = 64;
data[func_data_start..func_data_start+4].copy_from_slice(&1u32.to_le_bytes()); // 1 function data[func_data_start..func_data_start + 4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
let entry_start = func_data_start + 4; let entry_start = func_data_start + 4;
data[entry_start..entry_start+4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5 data[entry_start..entry_start + 4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
data[entry_start+4..entry_start+8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10 data[entry_start + 4..entry_start + 8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
// 5 + 10 = 15 > 10 (code section length) // 5 + 10 = 15 > 10 (code section length)
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidFunctionIndex)); assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidFunctionIndex));

View File

@ -7,7 +7,6 @@
#[repr(u16)] #[repr(u16)]
pub enum OpCode { pub enum OpCode {
// --- 6.1 Execution Control --- // --- 6.1 Execution Control ---
/// No operation. Does nothing for 1 cycle. /// No operation. Does nothing for 1 cycle.
Nop = 0x00, Nop = 0x00,
/// Stops the Virtual Machine execution immediately. /// Stops the Virtual Machine execution immediately.
@ -27,7 +26,6 @@ pub enum OpCode {
Trap = 0x05, Trap = 0x05,
// --- 6.2 Stack Manipulation --- // --- 6.2 Stack Manipulation ---
/// Loads a constant from the Constant Pool into the stack. /// Loads a constant from the Constant Pool into the stack.
/// Operand: index (u32) /// Operand: index (u32)
/// Stack: [] -> [value] /// Stack: [] -> [value]
@ -62,7 +60,6 @@ pub enum OpCode {
PushBounded = 0x19, PushBounded = 0x19,
// --- 6.3 Arithmetic --- // --- 6.3 Arithmetic ---
/// Adds the two top values (a + b). /// Adds the two top values (a + b).
/// Stack: [a, b] -> [result] /// Stack: [a, b] -> [result]
Add = 0x20, Add = 0x20,
@ -86,7 +83,6 @@ pub enum OpCode {
IntToBoundChecked = 0x26, IntToBoundChecked = 0x26,
// --- 6.4 Comparison and Logic --- // --- 6.4 Comparison and Logic ---
/// Checks if a equals b. /// Checks if a equals b.
/// Stack: [a, b] -> [bool] /// Stack: [a, b] -> [bool]
Eq = 0x30, Eq = 0x30,
@ -134,7 +130,6 @@ pub enum OpCode {
Neg = 0x3E, Neg = 0x3E,
// --- 6.5 Variables --- // --- 6.5 Variables ---
/// Loads a value from a global variable slot. /// Loads a value from a global variable slot.
/// Operand: slot_index (u32) /// Operand: slot_index (u32)
/// Stack: [] -> [value] /// Stack: [] -> [value]
@ -153,7 +148,6 @@ pub enum OpCode {
SetLocal = 0x43, SetLocal = 0x43,
// --- 6.6 Functions --- // --- 6.6 Functions ---
/// Calls a function by its index in the function table. /// Calls a function by its index in the function table.
/// Operand: func_id (u32) /// Operand: func_id (u32)
/// Stack: [arg0, arg1, ...] -> [return_slots...] /// Stack: [arg0, arg1, ...] -> [return_slots...]
@ -167,7 +161,6 @@ pub enum OpCode {
PopScope = 0x53, PopScope = 0x53,
// --- 6.7 HIP (Heap Interface Protocol) --- // --- 6.7 HIP (Heap Interface Protocol) ---
/// Allocates `slots` slots on the heap with the given `type_id`. /// Allocates `slots` slots on the heap with the given `type_id`.
/// Operands: type_id (u32), slots (u32) /// Operands: type_id (u32), slots (u32)
/// Stack: [] -> [gate] /// Stack: [] -> [gate]
@ -208,7 +201,6 @@ pub enum OpCode {
GateRelease = 0x6A, GateRelease = 0x6A,
// --- 6.8 Peripherals and System --- // --- 6.8 Peripherals and System ---
/// Invokes a system function (Firmware/OS). /// Invokes a system function (Firmware/OS).
/// Operand: syscall_id (u32) /// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall) /// Stack: [args...] -> [results...] (depends on syscall)

View File

@ -20,65 +20,537 @@ pub trait OpCodeSpecExt {
impl OpCodeSpecExt for OpCode { impl OpCodeSpecExt for OpCode {
fn spec(&self) -> OpcodeSpec { fn spec(&self) -> OpcodeSpec {
match self { match self {
OpCode::Nop => OpcodeSpec { name: "NOP", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, OpCode::Nop => OpcodeSpec {
OpCode::Halt => OpcodeSpec { name: "HALT", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: true, may_trap: false }, name: "NOP",
OpCode::Jmp => OpcodeSpec { name: "JMP", imm_bytes: 4, pops: 0, pushes: 0, is_branch: true, is_terminator: true, may_trap: false }, imm_bytes: 0,
OpCode::JmpIfFalse => OpcodeSpec { name: "JMP_IF_FALSE", imm_bytes: 4, pops: 1, pushes: 0, is_branch: true, is_terminator: false, may_trap: true }, pops: 0,
OpCode::JmpIfTrue => OpcodeSpec { name: "JMP_IF_TRUE", imm_bytes: 4, pops: 1, pushes: 0, is_branch: true, is_terminator: false, may_trap: true }, pushes: 0,
OpCode::Trap => OpcodeSpec { name: "TRAP", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: true, may_trap: true }, is_branch: false,
OpCode::PushConst => OpcodeSpec { name: "PUSH_CONST", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, is_terminator: false,
OpCode::Pop => OpcodeSpec { name: "POP", imm_bytes: 0, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, may_trap: false,
OpCode::PopN => OpcodeSpec { name: "POP_N", imm_bytes: 4, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, },
OpCode::Dup => OpcodeSpec { name: "DUP", imm_bytes: 0, pops: 1, pushes: 2, is_branch: false, is_terminator: false, may_trap: false }, OpCode::Halt => OpcodeSpec {
OpCode::Swap => OpcodeSpec { name: "SWAP", imm_bytes: 0, pops: 2, pushes: 2, is_branch: false, is_terminator: false, may_trap: false }, name: "HALT",
OpCode::PushI64 => OpcodeSpec { name: "PUSH_I64", imm_bytes: 8, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, imm_bytes: 0,
OpCode::PushF64 => OpcodeSpec { name: "PUSH_F64", imm_bytes: 8, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, pops: 0,
OpCode::PushBool => OpcodeSpec { name: "PUSH_BOOL", imm_bytes: 1, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, pushes: 0,
OpCode::PushI32 => OpcodeSpec { name: "PUSH_I32", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, is_branch: false,
OpCode::PushBounded => OpcodeSpec { name: "PUSH_BOUNDED", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, is_terminator: true,
OpCode::Add => OpcodeSpec { name: "ADD", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, may_trap: false,
OpCode::Sub => OpcodeSpec { name: "SUB", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, },
OpCode::Mul => OpcodeSpec { name: "MUL", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, OpCode::Jmp => OpcodeSpec {
OpCode::Div => OpcodeSpec { name: "DIV", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, name: "JMP",
OpCode::Mod => OpcodeSpec { name: "MOD", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, imm_bytes: 4,
OpCode::BoundToInt => OpcodeSpec { name: "BOUND_TO_INT", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, pops: 0,
OpCode::IntToBoundChecked => OpcodeSpec { name: "INT_TO_BOUND_CHECKED", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, pushes: 0,
OpCode::Eq => OpcodeSpec { name: "EQ", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, is_branch: true,
OpCode::Neq => OpcodeSpec { name: "NEQ", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, is_terminator: true,
OpCode::Lt => OpcodeSpec { name: "LT", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, may_trap: false,
OpCode::Gt => OpcodeSpec { name: "GT", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, },
OpCode::And => OpcodeSpec { name: "AND", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, OpCode::JmpIfFalse => OpcodeSpec {
OpCode::Or => OpcodeSpec { name: "OR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, name: "JMP_IF_FALSE",
OpCode::Not => OpcodeSpec { name: "NOT", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, imm_bytes: 4,
OpCode::BitAnd => OpcodeSpec { name: "BIT_AND", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, pops: 1,
OpCode::BitOr => OpcodeSpec { name: "BIT_OR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, pushes: 0,
OpCode::BitXor => OpcodeSpec { name: "BIT_XOR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, is_branch: true,
OpCode::Shl => OpcodeSpec { name: "SHL", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, is_terminator: false,
OpCode::Shr => OpcodeSpec { name: "SHR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, may_trap: true,
OpCode::Lte => OpcodeSpec { name: "LTE", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, },
OpCode::Gte => OpcodeSpec { name: "GTE", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, OpCode::JmpIfTrue => OpcodeSpec {
OpCode::Neg => OpcodeSpec { name: "NEG", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, name: "JMP_IF_TRUE",
OpCode::GetGlobal => OpcodeSpec { name: "GET_GLOBAL", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, imm_bytes: 4,
OpCode::SetGlobal => OpcodeSpec { name: "SET_GLOBAL", imm_bytes: 4, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, pops: 1,
OpCode::GetLocal => OpcodeSpec { name: "GET_LOCAL", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false }, pushes: 0,
OpCode::SetLocal => OpcodeSpec { name: "SET_LOCAL", imm_bytes: 4, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, is_branch: true,
OpCode::Call => OpcodeSpec { name: "CALL", imm_bytes: 4, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: true }, is_terminator: false,
OpCode::Ret => OpcodeSpec { name: "RET", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: true, may_trap: false }, may_trap: true,
OpCode::PushScope => OpcodeSpec { name: "PUSH_SCOPE", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, },
OpCode::PopScope => OpcodeSpec { name: "POP_SCOPE", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, OpCode::Trap => OpcodeSpec {
OpCode::Alloc => OpcodeSpec { name: "ALLOC", imm_bytes: 8, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, name: "TRAP",
OpCode::GateLoad => OpcodeSpec { name: "GATE_LOAD", imm_bytes: 4, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, imm_bytes: 0,
OpCode::GateStore => OpcodeSpec { name: "GATE_STORE", imm_bytes: 4, pops: 2, pushes: 0, is_branch: false, is_terminator: false, may_trap: true }, pops: 0,
OpCode::GateBeginPeek => OpcodeSpec { name: "GATE_BEGIN_PEEK", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, pushes: 0,
OpCode::GateEndPeek => OpcodeSpec { name: "GATE_END_PEEK", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, is_branch: false,
OpCode::GateBeginBorrow => OpcodeSpec { name: "GATE_BEGIN_BORROW", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, is_terminator: true,
OpCode::GateEndBorrow => OpcodeSpec { name: "GATE_END_BORROW", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, may_trap: true,
OpCode::GateBeginMutate => OpcodeSpec { name: "GATE_BEGIN_MUTATE", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, },
OpCode::GateEndMutate => OpcodeSpec { name: "GATE_END_MUTATE", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, OpCode::PushConst => OpcodeSpec {
OpCode::GateRetain => OpcodeSpec { name: "GATE_RETAIN", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true }, name: "PUSH_CONST",
OpCode::GateRelease => OpcodeSpec { name: "GATE_RELEASE", imm_bytes: 0, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: true }, imm_bytes: 4,
OpCode::Syscall => OpcodeSpec { name: "SYSCALL", imm_bytes: 4, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: true }, pops: 0,
OpCode::FrameSync => OpcodeSpec { name: "FRAME_SYNC", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false }, pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Pop => OpcodeSpec {
name: "POP",
imm_bytes: 0,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PopN => OpcodeSpec {
name: "POP_N",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Dup => OpcodeSpec {
name: "DUP",
imm_bytes: 0,
pops: 1,
pushes: 2,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Swap => OpcodeSpec {
name: "SWAP",
imm_bytes: 0,
pops: 2,
pushes: 2,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushI64 => OpcodeSpec {
name: "PUSH_I64",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushF64 => OpcodeSpec {
name: "PUSH_F64",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushBool => OpcodeSpec {
name: "PUSH_BOOL",
imm_bytes: 1,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushI32 => OpcodeSpec {
name: "PUSH_I32",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushBounded => OpcodeSpec {
name: "PUSH_BOUNDED",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Add => OpcodeSpec {
name: "ADD",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Sub => OpcodeSpec {
name: "SUB",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Mul => OpcodeSpec {
name: "MUL",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Div => OpcodeSpec {
name: "DIV",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Mod => OpcodeSpec {
name: "MOD",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::BoundToInt => OpcodeSpec {
name: "BOUND_TO_INT",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::IntToBoundChecked => OpcodeSpec {
name: "INT_TO_BOUND_CHECKED",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Eq => OpcodeSpec {
name: "EQ",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Neq => OpcodeSpec {
name: "NEQ",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Lt => OpcodeSpec {
name: "LT",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Gt => OpcodeSpec {
name: "GT",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::And => OpcodeSpec {
name: "AND",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Or => OpcodeSpec {
name: "OR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Not => OpcodeSpec {
name: "NOT",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::BitAnd => OpcodeSpec {
name: "BIT_AND",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::BitOr => OpcodeSpec {
name: "BIT_OR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::BitXor => OpcodeSpec {
name: "BIT_XOR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Shl => OpcodeSpec {
name: "SHL",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Shr => OpcodeSpec {
name: "SHR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Lte => OpcodeSpec {
name: "LTE",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Gte => OpcodeSpec {
name: "GTE",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Neg => OpcodeSpec {
name: "NEG",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::GetGlobal => OpcodeSpec {
name: "GET_GLOBAL",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::SetGlobal => OpcodeSpec {
name: "SET_GLOBAL",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::GetLocal => OpcodeSpec {
name: "GET_LOCAL",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::SetLocal => OpcodeSpec {
name: "SET_LOCAL",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Call => OpcodeSpec {
name: "CALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Ret => OpcodeSpec {
name: "RET",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: false,
},
OpCode::PushScope => OpcodeSpec {
name: "PUSH_SCOPE",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PopScope => OpcodeSpec {
name: "POP_SCOPE",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Alloc => OpcodeSpec {
name: "ALLOC",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateLoad => OpcodeSpec {
name: "GATE_LOAD",
imm_bytes: 4,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateStore => OpcodeSpec {
name: "GATE_STORE",
imm_bytes: 4,
pops: 2,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateBeginPeek => OpcodeSpec {
name: "GATE_BEGIN_PEEK",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateEndPeek => OpcodeSpec {
name: "GATE_END_PEEK",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateBeginBorrow => OpcodeSpec {
name: "GATE_BEGIN_BORROW",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateEndBorrow => OpcodeSpec {
name: "GATE_END_BORROW",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateBeginMutate => OpcodeSpec {
name: "GATE_BEGIN_MUTATE",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateEndMutate => OpcodeSpec {
name: "GATE_END_MUTATE",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateRetain => OpcodeSpec {
name: "GATE_RETAIN",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateRelease => OpcodeSpec {
name: "GATE_RELEASE",
imm_bytes: 0,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Syscall => OpcodeSpec {
name: "SYSCALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::FrameSync => OpcodeSpec {
name: "FRAME_SYNC",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
} }
} }
} }

View File

@ -1,8 +1,8 @@
use crate::abi::TrapInfo; use crate::abi::TrapInfo;
use std::collections::HashMap;
use std::sync::Arc;
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta}; use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
use crate::value::Value; use crate::value::Value;
use std::collections::HashMap;
use std::sync::Arc;
/// Represents a fully linked, executable PBS program image. /// Represents a fully linked, executable PBS program image.
/// ///
@ -22,7 +22,13 @@ pub struct ProgramImage {
} }
impl ProgramImage { impl ProgramImage {
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>, exports: HashMap<String, u32>) -> Self { pub fn new(
rom: Vec<u8>,
constant_pool: Vec<Value>,
functions: Vec<FunctionMeta>,
debug_info: Option<DebugInfo>,
exports: HashMap<String, u32>,
) -> Self {
Self { Self {
rom: Arc::from(rom), rom: Arc::from(rom),
constant_pool: Arc::from(constant_pool), constant_pool: Arc::from(constant_pool),
@ -33,9 +39,10 @@ impl ProgramImage {
} }
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo { pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
let span = self.debug_info.as_ref().and_then(|di| { let span = self
di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()) .debug_info
}); .as_ref()
.and_then(|di| di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()));
if let Some(func_idx) = self.find_function_index(pc) { if let Some(func_idx) = self.find_function_index(pc) {
if let Some(func_name) = self.get_function_name(func_idx) { if let Some(func_name) = self.get_function_name(func_idx) {
@ -43,23 +50,16 @@ impl ProgramImage {
} }
} }
TrapInfo { TrapInfo { code, opcode, message, pc, span }
code,
opcode,
message,
pc,
span,
}
} }
pub fn find_function_index(&self, pc: u32) -> Option<usize> { pub fn find_function_index(&self, pc: u32) -> Option<usize> {
self.functions.iter().position(|f| { self.functions.iter().position(|f| pc >= f.code_offset && pc < (f.code_offset + f.code_len))
pc >= f.code_offset && pc < (f.code_offset + f.code_len)
})
} }
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> { pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
self.debug_info.as_ref() self.debug_info
.as_ref()
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx)) .and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
.map(|(_, name)| name.as_str()) .map(|(_, name)| name.as_str())
} }
@ -67,35 +67,34 @@ impl ProgramImage {
impl From<BytecodeModule> for ProgramImage { impl From<BytecodeModule> for ProgramImage {
fn from(module: BytecodeModule) -> Self { fn from(module: BytecodeModule) -> Self {
let constant_pool: Vec<Value> = module.const_pool.iter().map(|entry| { let constant_pool: Vec<Value> = module
match entry { .const_pool
.iter()
.map(|entry| match entry {
ConstantPoolEntry::Null => Value::Null, ConstantPoolEntry::Null => Value::Null,
ConstantPoolEntry::Int64(v) => Value::Int64(*v), ConstantPoolEntry::Int64(v) => Value::Int64(*v),
ConstantPoolEntry::Float64(v) => Value::Float(*v), ConstantPoolEntry::Float64(v) => Value::Float(*v),
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v), ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
ConstantPoolEntry::String(v) => Value::String(v.clone()), ConstantPoolEntry::String(v) => Value::String(v.clone()),
ConstantPoolEntry::Int32(v) => Value::Int32(*v), ConstantPoolEntry::Int32(v) => Value::Int32(*v),
} })
}).collect(); .collect();
let mut exports = HashMap::new(); let mut exports = HashMap::new();
for export in module.exports { for export in module.exports {
exports.insert(export.symbol, export.func_idx); exports.insert(export.symbol, export.func_idx);
} }
ProgramImage::new( ProgramImage::new(module.code, constant_pool, module.functions, module.debug_info, exports)
module.code,
constant_pool,
module.functions,
module.debug_info,
exports,
)
} }
} }
impl From<ProgramImage> for BytecodeModule { impl From<ProgramImage> for BytecodeModule {
fn from(program: ProgramImage) -> Self { fn from(program: ProgramImage) -> Self {
let const_pool = program.constant_pool.iter().map(|v| match v { let const_pool = program
.constant_pool
.iter()
.map(|v| match v {
Value::Null => ConstantPoolEntry::Null, Value::Null => ConstantPoolEntry::Null,
Value::Int64(v) => ConstantPoolEntry::Int64(*v), Value::Int64(v) => ConstantPoolEntry::Int64(*v),
Value::Float(v) => ConstantPoolEntry::Float64(*v), Value::Float(v) => ConstantPoolEntry::Float64(*v),
@ -104,12 +103,14 @@ impl From<ProgramImage> for BytecodeModule {
Value::Int32(v) => ConstantPoolEntry::Int32(*v), Value::Int32(v) => ConstantPoolEntry::Int32(*v),
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32), Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
Value::Gate(_) => ConstantPoolEntry::Null, Value::Gate(_) => ConstantPoolEntry::Null,
}).collect(); })
.collect();
let exports = program.exports.iter().map(|(symbol, &func_idx)| Export { let exports = program
symbol: symbol.clone(), .exports
func_idx, .iter()
}).collect(); .map(|(symbol, &func_idx)| Export { symbol: symbol.clone(), func_idx })
.collect();
BytecodeModule { BytecodeModule {
version: 0, version: 0,

View File

@ -1,14 +1,16 @@
use std::collections::HashMap; use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Instant;
use prometeu_hal::asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
use prometeu_hal::AssetBridge; use prometeu_hal::AssetBridge;
use prometeu_hal::asset::{
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::sample::Sample; use prometeu_hal::sample::Sample;
use prometeu_hal::sound_bank::SoundBank; use prometeu_hal::sound_bank::SoundBank;
use prometeu_hal::tile_bank::{TileBank, TileSize}; use prometeu_hal::tile_bank::{TileBank, TileSize};
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller}; use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Instant;
/// Resident metadata for a decoded/materialized asset inside a BankPolicy. /// Resident metadata for a decoded/materialized asset inside a BankPolicy.
#[derive(Debug)] #[derive(Debug)]
@ -21,7 +23,6 @@ pub struct ResidentEntry<T> {
// /// Pin count (optional): if > 0, entry should not be evicted by policy. // /// Pin count (optional): if > 0, entry should not be evicted by policy.
// pub pins: u32, // pub pins: u32,
/// Telemetry / profiling fields (optional but useful). /// Telemetry / profiling fields (optional but useful).
pub loads: u64, pub loads: u64,
pub last_used: Instant, pub last_used: Instant,
@ -129,16 +130,41 @@ struct LoadHandleInfo {
} }
impl AssetBridge for AssetManager { impl AssetBridge for AssetManager {
fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) { self.initialize_for_cartridge(assets, preload, assets_data) } fn initialize_for_cartridge(
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> { self.load(asset_name, slot) } &self,
fn status(&self, handle: HandleId) -> LoadStatus { self.status(handle) } assets: Vec<AssetEntry>,
fn commit(&self, handle: HandleId) { self.commit(handle) } preload: Vec<PreloadEntry>,
fn cancel(&self, handle: HandleId) { self.cancel(handle) } assets_data: Vec<u8>,
fn apply_commits(&self) { self.apply_commits() } ) {
fn bank_info(&self, kind: BankType) -> BankStats { self.bank_info(kind) } self.initialize_for_cartridge(assets, preload, assets_data)
fn slot_info(&self, slot: SlotRef) -> SlotStats { self.slot_info(slot) } }
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> { self.find_slot_by_name(asset_name, kind) } fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> {
fn shutdown(&self) { self.shutdown() } self.load(asset_name, slot)
}
fn status(&self, handle: HandleId) -> LoadStatus {
self.status(handle)
}
fn commit(&self, handle: HandleId) {
self.commit(handle)
}
fn cancel(&self, handle: HandleId) {
self.cancel(handle)
}
fn apply_commits(&self) {
self.apply_commits()
}
fn bank_info(&self, kind: BankType) -> BankStats {
self.bank_info(kind)
}
fn slot_info(&self, slot: SlotRef) -> SlotStats {
self.slot_info(slot)
}
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> {
self.find_slot_by_name(asset_name, kind)
}
fn shutdown(&self) {
self.shutdown()
}
} }
impl AssetManager { impl AssetManager {
@ -171,7 +197,12 @@ impl AssetManager {
} }
} }
pub fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) { pub fn initialize_for_cartridge(
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: Vec<u8>,
) {
self.shutdown(); self.shutdown();
{ {
let mut asset_map = self.assets.write().unwrap(); let mut asset_map = self.assets.write().unwrap();
@ -190,45 +221,70 @@ impl AssetManager {
let entry_opt = { let entry_opt = {
let assets = self.assets.read().unwrap(); let assets = self.assets.read().unwrap();
let name_to_id = self.name_to_id.read().unwrap(); let name_to_id = self.name_to_id.read().unwrap();
name_to_id.get(&item.asset_name) name_to_id.get(&item.asset_name).and_then(|id| assets.get(id)).cloned()
.and_then(|id| assets.get(id))
.cloned()
}; };
if let Some(entry) = entry_opt { if let Some(entry) = entry_opt {
let slot_index = item.slot; let slot_index = item.slot;
match entry.bank_type { match entry.bank_type {
BankType::TILES => { BankType::TILES => {
if let Ok(bank) = Self::perform_load_tile_bank(&entry, self.assets_data.clone()) { if let Ok(bank) =
Self::perform_load_tile_bank(&entry, self.assets_data.clone())
{
let bank_arc = Arc::new(bank); let bank_arc = Arc::new(bank);
self.gfx_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize); self.gfx_policy.put_resident(
entry.asset_id,
Arc::clone(&bank_arc),
entry.decoded_size as usize,
);
self.gfx_installer.install_tile_bank(slot_index, bank_arc); self.gfx_installer.install_tile_bank(slot_index, bank_arc);
let mut slots = self.gfx_slots.write().unwrap(); let mut slots = self.gfx_slots.write().unwrap();
if slot_index < slots.len() { if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id); slots[slot_index] = Some(entry.asset_id);
} }
println!("[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index); println!(
"[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}",
entry.asset_name, entry.asset_id, slot_index
);
} else { } else {
eprintln!("[AssetManager] Failed to preload tile asset '{}'", entry.asset_name); eprintln!(
"[AssetManager] Failed to preload tile asset '{}'",
entry.asset_name
);
} }
} }
BankType::SOUNDS => { BankType::SOUNDS => {
if let Ok(bank) = Self::perform_load_sound_bank(&entry, self.assets_data.clone()) { if let Ok(bank) =
Self::perform_load_sound_bank(&entry, self.assets_data.clone())
{
let bank_arc = Arc::new(bank); let bank_arc = Arc::new(bank);
self.sound_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize); self.sound_policy.put_resident(
entry.asset_id,
Arc::clone(&bank_arc),
entry.decoded_size as usize,
);
self.sound_installer.install_sound_bank(slot_index, bank_arc); self.sound_installer.install_sound_bank(slot_index, bank_arc);
let mut slots = self.sound_slots.write().unwrap(); let mut slots = self.sound_slots.write().unwrap();
if slot_index < slots.len() { if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id); slots[slot_index] = Some(entry.asset_id);
} }
println!("[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index); println!(
"[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}",
entry.asset_name, entry.asset_id, slot_index
);
} else { } else {
eprintln!("[AssetManager] Failed to preload sound asset '{}'", entry.asset_name); eprintln!(
"[AssetManager] Failed to preload sound asset '{}'",
entry.asset_name
);
} }
} }
} }
} else { } else {
eprintln!("[AssetManager] Preload failed: asset '{}' not found in table", item.asset_name); eprintln!(
"[AssetManager] Preload failed: asset '{}' not found in table",
item.asset_name
);
} }
} }
} }
@ -237,7 +293,9 @@ impl AssetManager {
let entry = { let entry = {
let assets = self.assets.read().unwrap(); let assets = self.assets.read().unwrap();
let name_to_id = self.name_to_id.read().unwrap(); let name_to_id = self.name_to_id.read().unwrap();
let id = name_to_id.get(asset_name).ok_or_else(|| format!("Asset not found: {}", asset_name))?; let id = name_to_id
.get(asset_name)
.ok_or_else(|| format!("Asset not found: {}", asset_name))?;
assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone() assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone()
}; };
let asset_id = entry.asset_id; let asset_id = entry.asset_id;
@ -256,31 +314,33 @@ impl AssetManager {
if let Some(bank) = self.gfx_policy.get_resident(asset_id) { if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
self.gfx_policy.stage(handle_id, bank); self.gfx_policy.stage(handle_id, bank);
true true
} else { false } } else {
false
}
} }
BankType::SOUNDS => { BankType::SOUNDS => {
if let Some(bank) = self.sound_policy.get_resident(asset_id) { if let Some(bank) = self.sound_policy.get_resident(asset_id) {
self.sound_policy.stage(handle_id, bank); self.sound_policy.stage(handle_id, bank);
true true
} else { false } } else {
false
}
} }
}; };
if already_resident { if already_resident {
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo { self.handles.write().unwrap().insert(
_asset_id: asset_id, handle_id,
slot, LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::READY },
status: LoadStatus::READY, );
});
return Ok(handle_id); return Ok(handle_id);
} }
// Not resident, start loading // Not resident, start loading
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo { self.handles.write().unwrap().insert(
_asset_id: asset_id, handle_id,
slot, LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::PENDING },
status: LoadStatus::PENDING, );
});
let handles = self.handles.clone(); let handles = self.handles.clone();
let assets_data = self.assets_data.clone(); let assets_data = self.assets_data.clone();
@ -319,7 +379,10 @@ impl AssetManager {
existing.loads += 1; existing.loads += 1;
Arc::clone(&existing.value) Arc::clone(&existing.value)
} else { } else {
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize); let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize,
);
map.insert(asset_id, entry); map.insert(asset_id, entry);
bank_arc bank_arc
} }
@ -327,11 +390,15 @@ impl AssetManager {
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc); gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc);
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; } if h.status == LoadStatus::LOADING {
h.status = LoadStatus::READY;
}
} }
} else { } else {
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; } if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
} }
} }
BankType::SOUNDS => { BankType::SOUNDS => {
@ -345,7 +412,10 @@ impl AssetManager {
existing.loads += 1; existing.loads += 1;
Arc::clone(&existing.value) Arc::clone(&existing.value)
} else { } else {
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize); let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize,
);
map.insert(asset_id, entry); map.insert(asset_id, entry);
bank_arc bank_arc
} }
@ -353,11 +423,15 @@ impl AssetManager {
sound_policy_staging.write().unwrap().insert(handle_id, resident_arc); sound_policy_staging.write().unwrap().insert(handle_id, resident_arc);
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; } if h.status == LoadStatus::LOADING {
h.status = LoadStatus::READY;
}
} }
} else { } else {
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; } if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
} }
} }
} }
@ -366,7 +440,10 @@ impl AssetManager {
Ok(handle_id) Ok(handle_id)
} }
fn perform_load_tile_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<TileBank, String> { fn perform_load_tile_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<Vec<u8>>>,
) -> Result<TileBank, String> {
if entry.codec != "RAW" { if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec)); return Err(format!("Unsupported codec: {}", entry.codec));
} }
@ -383,9 +460,12 @@ impl AssetManager {
let buffer = &assets_data[start..end]; let buffer = &assets_data[start..end];
// Decode TILEBANK metadata // Decode TILEBANK metadata
let tile_size_val = entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?; let tile_size_val =
let width = entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize; entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
let height = entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize; let width =
entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
let height =
entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
let tile_size = match tile_size_val { let tile_size = match tile_size_val {
8 => TileSize::Size8, 8 => TileSize::Size8,
@ -406,21 +486,19 @@ impl AssetManager {
for p in 0..64 { for p in 0..64 {
for c in 0..16 { for c in 0..16 {
let offset = (p * 16 + c) * 2; let offset = (p * 16 + c) * 2;
let color_raw = u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]); let color_raw =
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
palettes[p][c] = Color(color_raw); palettes[p][c] = Color(color_raw);
} }
} }
Ok(TileBank { Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
tile_size,
width,
height,
pixel_indices,
palettes,
})
} }
fn perform_load_sound_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<SoundBank, String> { fn perform_load_sound_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<Vec<u8>>>,
) -> Result<SoundBank, String> {
if entry.codec != "RAW" { if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec)); return Err(format!("Unsupported codec: {}", entry.codec));
} }
@ -436,12 +514,13 @@ impl AssetManager {
let buffer = &assets_data[start..end]; let buffer = &assets_data[start..end];
let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32; let sample_rate =
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
let mut data = Vec::with_capacity(buffer.len() / 2); let mut data = Vec::with_capacity(buffer.len() / 2);
for i in (0..buffer.len()).step_by(2) { for i in (0..buffer.len()).step_by(2) {
if i + 1 < buffer.len() { if i + 1 < buffer.len() {
data.push(i16::from_le_bytes([buffer[i], buffer[i+1]])); data.push(i16::from_le_bytes([buffer[i], buffer[i + 1]]));
} }
} }
@ -540,7 +619,9 @@ impl AssetManager {
{ {
let slots = self.gfx_slots.read().unwrap(); let slots = self.gfx_slots.read().unwrap();
for s in slots.iter() { for s in slots.iter() {
if s.is_some() { slots_occupied += 1; } if s.is_some() {
slots_occupied += 1;
}
} }
} }
@ -581,7 +662,9 @@ impl AssetManager {
{ {
let slots = self.sound_slots.read().unwrap(); let slots = self.sound_slots.read().unwrap();
for s in slots.iter() { for s in slots.iter() {
if s.is_some() { slots_occupied += 1; } if s.is_some() {
slots_occupied += 1;
}
} }
} }
@ -604,7 +687,11 @@ impl AssetManager {
let asset_id = slots.get(slot.index).and_then(|s| s.clone()); let asset_id = slots.get(slot.index).and_then(|s| s.clone());
let (bytes, asset_name) = if let Some(id) = &asset_id { let (bytes, asset_name) = if let Some(id) = &asset_id {
let bytes = self.gfx_policy.resident.read().unwrap() let bytes = self
.gfx_policy
.resident
.read()
.unwrap()
.get(id) .get(id)
.map(|entry| entry.bytes) .map(|entry| entry.bytes)
.unwrap_or(0); .unwrap_or(0);
@ -614,19 +701,18 @@ impl AssetManager {
(0, None) (0, None)
}; };
SlotStats { SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
asset_id,
asset_name,
generation: 0,
resident_bytes: bytes,
}
} }
BankType::SOUNDS => { BankType::SOUNDS => {
let slots = self.sound_slots.read().unwrap(); let slots = self.sound_slots.read().unwrap();
let asset_id = slots.get(slot.index).and_then(|s| s.clone()); let asset_id = slots.get(slot.index).and_then(|s| s.clone());
let (bytes, asset_name) = if let Some(id) = &asset_id { let (bytes, asset_name) = if let Some(id) = &asset_id {
let bytes = self.sound_policy.resident.read().unwrap() let bytes = self
.sound_policy
.resident
.read()
.unwrap()
.get(id) .get(id)
.map(|entry| entry.bytes) .map(|entry| entry.bytes)
.unwrap_or(0); .unwrap_or(0);
@ -636,12 +722,7 @@ impl AssetManager {
(0, None) (0, None)
}; };
SlotStats { SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
asset_id,
asset_name,
generation: 0,
resident_bytes: bytes,
}
} }
} }
} }
@ -832,9 +913,7 @@ mod tests {
}), }),
}; };
let preload = vec![ let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }];
PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }
];
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
@ -868,9 +947,7 @@ mod tests {
metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }), metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }),
}; };
let preload = vec![ let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }];
PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }
];
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
am.initialize_for_cartridge(vec![asset_entry], preload, data); am.initialize_for_cartridge(vec![asset_entry], preload, data);

View File

@ -1,15 +1,15 @@
use std::sync::Arc;
use prometeu_hal::AudioBridge; use prometeu_hal::AudioBridge;
use std::sync::Arc;
/// Maximum number of simultaneous audio voices supported by the hardware. /// Maximum number of simultaneous audio voices supported by the hardware.
pub const MAX_CHANNELS: usize = 16; pub const MAX_CHANNELS: usize = 16;
/// Standard sample rate for the final audio output. /// Standard sample rate for the final audio output.
pub const OUTPUT_SAMPLE_RATE: u32 = 48000; pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
use crate::memory_banks::SoundBankPoolAccess;
/// Looping mode for samples (re-exported from the hardware contract). /// Looping mode for samples (re-exported from the hardware contract).
pub use prometeu_hal::LoopMode; pub use prometeu_hal::LoopMode;
use prometeu_hal::sample::Sample; use prometeu_hal::sample::Sample;
use crate::memory_banks::SoundBankPoolAccess;
/// State of a single playback voice (channel). /// State of a single playback voice (channel).
/// ///
@ -65,24 +65,13 @@ pub enum AudioCommand {
loop_mode: LoopMode, loop_mode: LoopMode,
}, },
/// Immediately stop playback on a voice. /// Immediately stop playback on a voice.
Stop { Stop { voice_id: usize },
voice_id: usize,
},
/// Update volume of an ongoing playback. /// Update volume of an ongoing playback.
SetVolume { SetVolume { voice_id: usize, volume: u8 },
voice_id: usize,
volume: u8,
},
/// Update panning of an ongoing playback. /// Update panning of an ongoing playback.
SetPan { SetPan { voice_id: usize, pan: u8 },
voice_id: usize,
pan: u8,
},
/// Update pitch of an ongoing playback. /// Update pitch of an ongoing playback.
SetPitch { SetPitch { voice_id: usize, pitch: f64 },
voice_id: usize,
pitch: f64,
},
/// Pause all audio processing. /// Pause all audio processing.
MasterPause, MasterPause,
/// Resume audio processing. /// Resume audio processing.
@ -114,20 +103,57 @@ pub struct Audio {
} }
impl AudioBridge for Audio { impl AudioBridge for Audio {
fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) { fn play(
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On }; &mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: prometeu_hal::LoopMode,
) {
let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On,
};
self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm) self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm)
} }
fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) { fn play_sample(
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On }; &mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: prometeu_hal::LoopMode,
) {
let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On,
};
self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm) self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm)
} }
fn stop(&mut self, voice_id: usize) { self.stop(voice_id) } fn stop(&mut self, voice_id: usize) {
fn set_volume(&mut self, voice_id: usize, volume: u8) { self.set_volume(voice_id, volume) } self.stop(voice_id)
fn set_pan(&mut self, voice_id: usize, pan: u8) { self.set_pan(voice_id, pan) } }
fn set_pitch(&mut self, voice_id: usize, pitch: f64) { self.set_pitch(voice_id, pitch) } fn set_volume(&mut self, voice_id: usize, volume: u8) {
fn is_playing(&self, voice_id: usize) -> bool { self.is_playing(voice_id) } self.set_volume(voice_id, volume)
fn clear_commands(&mut self) { self.clear_commands() } }
fn set_pan(&mut self, voice_id: usize, pan: u8) {
self.set_pan(voice_id, pan)
}
fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
self.set_pitch(voice_id, pitch)
}
fn is_playing(&self, voice_id: usize) -> bool {
self.is_playing(voice_id)
}
fn clear_commands(&mut self) {
self.clear_commands()
}
} }
impl Audio { impl Audio {
@ -140,25 +166,51 @@ impl Audio {
} }
} }
pub fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { pub fn play(
&mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) {
if voice_id >= MAX_CHANNELS { if voice_id >= MAX_CHANNELS {
return; return;
} }
// Resolve the sample from the hardware pools // Resolve the sample from the hardware pools
let sample = self.sound_banks.sound_bank_slot(bank_id as usize) let sample = self
.sound_banks
.sound_bank_slot(bank_id as usize)
.and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone)); .and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone));
if let Some(s) = sample { if let Some(s) = sample {
println!("[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.", bank_id, sample_id, voice_id); println!(
"[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.",
bank_id, sample_id, voice_id
);
self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode); self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode);
} else { } else {
eprintln!("[Audio] Failed to resolve sample from bank {} sample {}.", bank_id, sample_id); eprintln!(
"[Audio] Failed to resolve sample from bank {} sample {}.",
bank_id, sample_id
);
} }
} }
pub fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { pub fn play_sample(
&mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) {
if voice_id >= MAX_CHANNELS { if voice_id >= MAX_CHANNELS {
return; return;
} }
@ -217,11 +269,7 @@ impl Audio {
} }
pub fn is_playing(&self, voice_id: usize) -> bool { pub fn is_playing(&self, voice_id: usize) -> bool {
if voice_id < MAX_CHANNELS { if voice_id < MAX_CHANNELS { self.voices[voice_id].active } else { false }
self.voices[voice_id].active
} else {
false
}
} }
/// Clears the command queue. The Host should consume this every frame. /// Clears the command queue. The Host should consume this every frame.

View File

@ -1,11 +1,11 @@
use crate::memory_banks::TileBankPoolAccess; use crate::memory_banks::TileBankPoolAccess;
use std::sync::Arc;
use prometeu_hal::color::Color;
use prometeu_hal::GfxBridge; use prometeu_hal::GfxBridge;
use prometeu_hal::color::Color;
use prometeu_hal::sprite::Sprite; use prometeu_hal::sprite::Sprite;
use prometeu_hal::tile::Tile; use prometeu_hal::tile::Tile;
use prometeu_hal::tile_bank::{TileBank, TileSize}; use prometeu_hal::tile_bank::{TileBank, TileSize};
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap}; use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
use std::sync::Arc;
/// Blending modes inspired by classic 16-bit hardware. /// Blending modes inspired by classic 16-bit hardware.
/// Defines how source pixels are combined with existing pixels in the framebuffer. /// Defines how source pixels are combined with existing pixels in the framebuffer.
@ -79,44 +79,139 @@ pub struct Gfx {
} }
impl GfxBridge for Gfx { impl GfxBridge for Gfx {
fn size(&self) -> (usize, usize) { self.size() } fn size(&self) -> (usize, usize) {
fn front_buffer(&self) -> &[u16] { self.front_buffer() } self.size()
fn clear(&mut self, color: Color) { self.clear(color) } }
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: prometeu_hal::BlendMode) { let m = match mode { prometeu_hal::BlendMode::None => BlendMode::None, prometeu_hal::BlendMode::Half => BlendMode::Half, prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus, prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus, prometeu_hal::BlendMode::Full => BlendMode::Full }; self.fill_rect_blend(x, y, w, h, color, m) } fn front_buffer(&self) -> &[u16] {
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.fill_rect(x, y, w, h, color) } self.front_buffer()
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) { self.draw_pixel(x, y, color) } }
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { self.draw_line(x0, y0, x1, y1, color) } fn clear(&mut self, color: Color) {
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.draw_circle(xc, yc, r, color) } self.clear(color)
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_points(xc, yc, x, y, color) } }
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.fill_circle(xc, yc, r, color) } fn fill_rect_blend(
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_lines(xc, yc, x, y, color) } &mut self,
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { self.draw_disc(x, y, r, border_color, fill_color) } x: i32,
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.draw_rect(x, y, w, h, color) } y: i32,
fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) { self.draw_square(x, y, w, h, border_color, fill_color) } w: i32,
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) { self.draw_horizontal_line(x0, x1, y, color) } h: i32,
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) { self.draw_vertical_line(x, y0, y1, color) } color: Color,
fn present(&mut self) { self.present() } mode: prometeu_hal::BlendMode,
fn render_all(&mut self) { self.render_all() } ) {
fn render_layer(&mut self, layer_idx: usize) { self.render_layer(layer_idx) } let m = match mode {
fn render_hud(&mut self) { self.render_hud() } prometeu_hal::BlendMode::None => BlendMode::None,
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { self.draw_text(x, y, text, color) } prometeu_hal::BlendMode::Half => BlendMode::Half,
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) { self.draw_char(x, y, c, color) } prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus,
prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus,
prometeu_hal::BlendMode::Full => BlendMode::Full,
};
self.fill_rect_blend(x, y, w, h, color, m)
}
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.fill_rect(x, y, w, h, color)
}
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
self.draw_pixel(x, y, color)
}
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
self.draw_line(x0, y0, x1, y1, color)
}
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
self.draw_circle(xc, yc, r, color)
}
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_circle_points(xc, yc, x, y, color)
}
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
self.fill_circle(xc, yc, r, color)
}
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_circle_lines(xc, yc, x, y, color)
}
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
self.draw_disc(x, y, r, border_color, fill_color)
}
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.draw_rect(x, y, w, h, color)
}
fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
) {
self.draw_square(x, y, w, h, border_color, fill_color)
}
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
self.draw_horizontal_line(x0, x1, y, color)
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
self.draw_vertical_line(x, y0, y1, color)
}
fn present(&mut self) {
self.present()
}
fn render_all(&mut self) {
self.render_all()
}
fn render_layer(&mut self, layer_idx: usize) {
self.render_layer(layer_idx)
}
fn render_hud(&mut self) {
self.render_hud()
}
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
self.draw_text(x, y, text, color)
}
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
self.draw_char(x, y, c, color)
}
fn layer(&self, index: usize) -> &ScrollableTileLayer { &self.layers[index] } fn layer(&self, index: usize) -> &ScrollableTileLayer {
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer { &mut self.layers[index] } &self.layers[index]
fn hud(&self) -> &HudTileLayer { &self.hud } }
fn hud_mut(&mut self) -> &mut HudTileLayer { &mut self.hud } fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer {
fn sprite(&self, index: usize) -> &Sprite { &self.sprites[index] } &mut self.layers[index]
fn sprite_mut(&mut self, index: usize) -> &mut Sprite { &mut self.sprites[index] } }
fn hud(&self) -> &HudTileLayer {
&self.hud
}
fn hud_mut(&mut self) -> &mut HudTileLayer {
&mut self.hud
}
fn sprite(&self, index: usize) -> &Sprite {
&self.sprites[index]
}
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
&mut self.sprites[index]
}
fn scene_fade_level(&self) -> u8 { self.scene_fade_level } fn scene_fade_level(&self) -> u8 {
fn set_scene_fade_level(&mut self, level: u8) { self.scene_fade_level = level; } self.scene_fade_level
fn scene_fade_color(&self) -> Color { self.scene_fade_color } }
fn set_scene_fade_color(&mut self, color: Color) { self.scene_fade_color = color; } fn set_scene_fade_level(&mut self, level: u8) {
fn hud_fade_level(&self) -> u8 { self.hud_fade_level } self.scene_fade_level = level;
fn set_hud_fade_level(&mut self, level: u8) { self.hud_fade_level = level; } }
fn hud_fade_color(&self) -> Color { self.hud_fade_color } fn scene_fade_color(&self) -> Color {
fn set_hud_fade_color(&mut self, color: Color) { self.hud_fade_color = color; } self.scene_fade_color
}
fn set_scene_fade_color(&mut self, color: Color) {
self.scene_fade_color = color;
}
fn hud_fade_level(&self) -> u8 {
self.hud_fade_level
}
fn set_hud_fade_level(&mut self, level: u8) {
self.hud_fade_level = level;
}
fn hud_fade_color(&self) -> Color {
self.hud_fade_color
}
fn set_hud_fade_color(&mut self, color: Color) {
self.hud_fade_color = color;
}
} }
impl Gfx { impl Gfx {
@ -187,7 +282,9 @@ impl Gfx {
color: Color, color: Color,
mode: BlendMode, mode: BlendMode,
) { ) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
let fw = self.w as i32; let fw = self.w as i32;
let fh = self.h as i32; let fh = self.h as i32;
@ -216,7 +313,9 @@ impl Gfx {
/// Draws a single pixel. /// Draws a single pixel.
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) { pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 { if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 {
self.back[y as usize * self.w + x as usize] = color.0; self.back[y as usize * self.w + x as usize] = color.0;
} }
@ -224,7 +323,9 @@ impl Gfx {
/// Draws a line between two points using Bresenham's algorithm. /// Draws a line between two points using Bresenham's algorithm.
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
let dx = (x1 - x0).abs(); let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 }; let sx = if x0 < x1 { 1 } else { -1 };
@ -237,7 +338,9 @@ impl Gfx {
loop { loop {
self.draw_pixel(x, y, color); self.draw_pixel(x, y, color);
if x == x1 && y == y1 { break; } if x == x1 && y == y1 {
break;
}
let e2 = 2 * err; let e2 = 2 * err;
if e2 >= dy { if e2 >= dy {
err += dy; err += dy;
@ -252,9 +355,13 @@ impl Gfx {
/// Draws a circle outline using Midpoint Circle Algorithm. /// Draws a circle outline using Midpoint Circle Algorithm.
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
if r < 0 { return; } if r < 0 {
return;
}
let mut x = 0; let mut x = 0;
let mut y = r; let mut y = r;
let mut d = 3 - 2 * r; let mut d = 3 - 2 * r;
@ -284,9 +391,13 @@ impl Gfx {
/// Draws a filled circle. /// Draws a filled circle.
pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
if r < 0 { return; } if r < 0 {
return;
}
let mut x = 0; let mut x = 0;
let mut y = r; let mut y = r;
let mut d = 3 - 2 * r; let mut d = 3 - 2 * r;
@ -318,9 +429,13 @@ impl Gfx {
/// Draws a rectangle outline. /// Draws a rectangle outline.
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
if w <= 0 || h <= 0 { return; } if w <= 0 || h <= 0 {
return;
}
self.draw_horizontal_line(x, x + w - 1, y, color); self.draw_horizontal_line(x, x + w - 1, y, color);
self.draw_horizontal_line(x, x + w - 1, y + h - 1, color); self.draw_horizontal_line(x, x + w - 1, y + h - 1, color);
self.draw_vertical_line(x, y, y + h - 1, color); self.draw_vertical_line(x, y, y + h - 1, color);
@ -328,30 +443,50 @@ impl Gfx {
} }
/// Draws a square (filled rectangle with border). /// Draws a square (filled rectangle with border).
pub fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) { pub fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
) {
self.fill_rect(x, y, w, h, fill_color); self.fill_rect(x, y, w, h, fill_color);
self.draw_rect(x, y, w, h, border_color); self.draw_rect(x, y, w, h, border_color);
} }
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) { fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
if y < 0 || y >= self.h as i32 { return; } if y < 0 || y >= self.h as i32 {
return;
}
let start = x0.max(0); let start = x0.max(0);
let end = x1.min(self.w as i32 - 1); let end = x1.min(self.w as i32 - 1);
if start > end { return; } if start > end {
return;
}
for x in start..=end { for x in start..=end {
self.back[y as usize * self.w + x as usize] = color.0; self.back[y as usize * self.w + x as usize] = color.0;
} }
} }
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) { fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY { return; } if color == Color::COLOR_KEY {
return;
}
if x < 0 || x >= self.w as i32 { return; } if x < 0 || x >= self.w as i32 {
return;
}
let start = y0.max(0); let start = y0.max(0);
let end = y1.min(self.h as i32 - 1); let end = y1.min(self.h as i32 - 1);
if start > end { return; } if start > end {
return;
}
for y in start..=end { for y in start..=end {
self.back[y as usize * self.w + x as usize] = color.0; self.back[y as usize * self.w + x as usize] = color.0;
} }
@ -382,18 +517,40 @@ impl Gfx {
} }
// 1. Priority 0 sprites: drawn at the very back, behind everything else. // 1. Priority 0 sprites: drawn at the very back, behind everything else.
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &*self.tile_banks); Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[0],
&self.sprites,
&*self.tile_banks,
);
// 2. Main layers and prioritized sprites. // 2. Main layers and prioritized sprites.
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ... // Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
for i in 0..self.layers.len() { for i in 0..self.layers.len() {
let bank_id = self.layers[i].bank_id as usize; let bank_id = self.layers[i].bank_id as usize;
if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) { if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) {
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, &bank, self.layers[i].scroll_x, self.layers[i].scroll_y); Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[i].map,
&bank,
self.layers[i].scroll_x,
self.layers[i].scroll_y,
);
} }
// Draw sprites that belong to this depth level // Draw sprites that belong to this depth level
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &*self.tile_banks); Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[i + 1],
&self.sprites,
&*self.tile_banks,
);
} }
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD). // 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
@ -408,7 +565,9 @@ impl Gfx {
/// Renders a specific game layer. /// Renders a specific game layer.
pub fn render_layer(&mut self, layer_idx: usize) { pub fn render_layer(&mut self, layer_idx: usize) {
if layer_idx >= self.layers.len() { return; } if layer_idx >= self.layers.len() {
return;
}
let bank_id = self.layers[layer_idx].bank_id as usize; let bank_id = self.layers[layer_idx].bank_id as usize;
let scroll_x = self.layers[layer_idx].scroll_x; let scroll_x = self.layers[layer_idx].scroll_x;
@ -419,7 +578,15 @@ impl Gfx {
_ => return, _ => return,
}; };
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, &bank, scroll_x, scroll_y); Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[layer_idx].map,
&bank,
scroll_x,
scroll_y,
);
} }
/// Renders the HUD (fixed position, no scroll). /// Renders the HUD (fixed position, no scroll).
@ -427,7 +594,13 @@ impl Gfx {
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks); Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
} }
fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, tile_banks: &dyn TileBankPoolAccess) { fn render_hud_with_pool(
back: &mut [u16],
w: usize,
h: usize,
hud: &HudTileLayer,
tile_banks: &dyn TileBankPoolAccess,
) {
let bank_id = hud.bank_id as usize; let bank_id = hud.bank_id as usize;
let bank = match tile_banks.tile_bank_slot(bank_id) { let bank = match tile_banks.tile_bank_slot(bank_id) {
Some(b) => b, Some(b) => b,
@ -445,7 +618,7 @@ impl Gfx {
map: &TileMap, map: &TileMap,
bank: &TileBank, bank: &TileBank,
scroll_x: i32, scroll_x: i32,
scroll_y: i32 scroll_y: i32,
) { ) {
let tile_size = bank.tile_size as usize; let tile_size = bank.tile_size as usize;
@ -469,34 +642,58 @@ impl Gfx {
let map_y = (start_tile_y + ty as i32) as usize; let map_y = (start_tile_y + ty as i32) as usize;
// Bounds check: don't draw if the camera is outside the map. // Bounds check: don't draw if the camera is outside the map.
if map_x >= map.width || map_y >= map.height { continue; } if map_x >= map.width || map_y >= map.height {
continue;
}
let tile = map.tiles[map_y * map.width + map_x]; let tile = map.tiles[map_y * map.width + map_x];
// Optimized skip for empty (ID 0) tiles. // Optimized skip for empty (ID 0) tiles.
if tile.id == 0 { continue; } if tile.id == 0 {
continue;
}
// 5. Project the tile pixels to screen space. // 5. Project the tile pixels to screen space.
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x; let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y; let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
Self::draw_tile_pixels(back, screen_w, screen_h, screen_tile_x, screen_tile_y, tile, bank); Self::draw_tile_pixels(
back,
screen_w,
screen_h,
screen_tile_x,
screen_tile_y,
tile,
bank,
);
} }
} }
} }
/// Internal helper to copy a single tile's pixels to the framebuffer. /// Internal helper to copy a single tile's pixels to the framebuffer.
/// Handles flipping and palette resolution. /// Handles flipping and palette resolution.
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: Tile, bank: &TileBank) { fn draw_tile_pixels(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
x: i32,
y: i32,
tile: Tile,
bank: &TileBank,
) {
let size = bank.tile_size as usize; let size = bank.tile_size as usize;
for local_y in 0..size { for local_y in 0..size {
let world_y = y + local_y as i32; let world_y = y + local_y as i32;
if world_y < 0 || world_y >= screen_h as i32 { continue; } if world_y < 0 || world_y >= screen_h as i32 {
continue;
}
for local_x in 0..size { for local_x in 0..size {
let world_x = x + local_x as i32; let world_x = x + local_x as i32;
if world_x < 0 || world_x >= screen_w as i32 { continue; } if world_x < 0 || world_x >= screen_w as i32 {
continue;
}
// Handle flip flags by reversing the fetch coordinates. // Handle flip flags by reversing the fetch coordinates.
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x }; let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
@ -506,7 +703,9 @@ impl Gfx {
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y); let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
// 2. Hardware rule: Color index 0 is always fully transparent. // 2. Hardware rule: Color index 0 is always fully transparent.
if px_index == 0 { continue; } if px_index == 0 {
continue;
}
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette. // 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
let color = bank.resolve_color(tile.palette_id, px_index); let color = bank.resolve_color(tile.palette_id, px_index);
@ -538,7 +737,7 @@ impl Gfx {
screen_w: usize, screen_w: usize,
screen_h: usize, screen_h: usize,
sprite: &Sprite, sprite: &Sprite,
bank: &TileBank bank: &TileBank,
) { ) {
// ... (same bounds/clipping calculation we already had) ... // ... (same bounds/clipping calculation we already had) ...
let size = bank.tile_size as usize; let size = bank.tile_size as usize;
@ -559,7 +758,9 @@ impl Gfx {
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y); let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
// 2. Transparency // 2. Transparency
if px_index == 0 { continue; } if px_index == 0 {
continue;
}
// 3. Resolve color via palette (from the tile inside the sprite) // 3. Resolve color via palette (from the tile inside the sprite)
let color = bank.resolve_color(sprite.tile.palette_id, px_index); let color = bank.resolve_color(sprite.tile.palette_id, px_index);
@ -572,7 +773,9 @@ impl Gfx {
/// Applies the fade effect to the entire back buffer. /// Applies the fade effect to the entire back buffer.
/// level: 0 (full color) to 31 (visible) /// level: 0 (full color) to 31 (visible)
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) { fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
if level >= 31 { return; } // Fully visible, skip processing if level >= 31 {
return;
} // Fully visible, skip processing
let weight = level as u16; let weight = level as u16;
let inv_weight = 31 - weight; let inv_weight = 31 - weight;

View File

@ -1,11 +1,14 @@
use std::sync::Arc;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use crate::asset::AssetManager; use crate::asset::AssetManager;
use crate::audio::Audio; use crate::audio::Audio;
use crate::gfx::Gfx; use crate::gfx::Gfx;
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess, TileBankPoolInstaller}; use crate::memory_banks::{
MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess,
TileBankPoolInstaller,
};
use crate::pad::Pad; use crate::pad::Pad;
use crate::touch::Touch; use crate::touch::Touch;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use std::sync::Arc;
/// Aggregate structure for all virtual hardware peripherals. /// Aggregate structure for all virtual hardware peripherals.
/// ///
@ -33,20 +36,40 @@ pub struct Hardware {
} }
impl HardwareBridge for Hardware { impl HardwareBridge for Hardware {
fn gfx(&self) -> &dyn GfxBridge { &self.gfx } fn gfx(&self) -> &dyn GfxBridge {
fn gfx_mut(&mut self) -> &mut dyn GfxBridge { &mut self.gfx } &self.gfx
}
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
&mut self.gfx
}
fn audio(&self) -> &dyn AudioBridge { &self.audio } fn audio(&self) -> &dyn AudioBridge {
fn audio_mut(&mut self) -> &mut dyn AudioBridge { &mut self.audio } &self.audio
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
&mut self.audio
}
fn pad(&self) -> &dyn PadBridge { &self.pad } fn pad(&self) -> &dyn PadBridge {
fn pad_mut(&mut self) -> &mut dyn PadBridge { &mut self.pad } &self.pad
}
fn pad_mut(&mut self) -> &mut dyn PadBridge {
&mut self.pad
}
fn touch(&self) -> &dyn TouchBridge { &self.touch } fn touch(&self) -> &dyn TouchBridge {
fn touch_mut(&mut self) -> &mut dyn TouchBridge { &mut self.touch } &self.touch
}
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
&mut self.touch
}
fn assets(&self) -> &dyn AssetBridge { &self.assets } fn assets(&self) -> &dyn AssetBridge {
fn assets_mut(&mut self) -> &mut dyn AssetBridge { &mut self.assets } &self.assets
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
&mut self.assets
}
} }
impl Hardware { impl Hardware {
@ -59,7 +82,11 @@ impl Hardware {
pub fn new() -> Self { pub fn new() -> Self {
let memory_banks = Arc::new(MemoryBanks::new()); let memory_banks = Arc::new(MemoryBanks::new());
Self { Self {
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>), gfx: Gfx::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>,
),
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>), audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
pad: Pad::default(), pad: Pad::default(),
touch: Touch::default(), touch: Touch::default(),

View File

@ -1,10 +1,10 @@
mod asset; mod asset;
mod audio;
mod gfx; mod gfx;
pub mod hardware;
mod memory_banks;
mod pad; mod pad;
mod touch; mod touch;
mod audio;
mod memory_banks;
pub mod hardware;
pub use crate::asset::AssetManager; pub use crate::asset::AssetManager;
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};

View File

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

View File

@ -1,5 +1,5 @@
use prometeu_hal::{InputSignals, PadBridge};
use prometeu_hal::button::Button; use prometeu_hal::button::Button;
use prometeu_hal::{InputSignals, PadBridge};
#[derive(Default, Clone, Copy, Debug)] #[derive(Default, Clone, Copy, Debug)]
pub struct Pad { pub struct Pad {
@ -20,21 +20,49 @@ pub struct Pad {
} }
impl PadBridge for Pad { impl PadBridge for Pad {
fn begin_frame(&mut self, signals: &InputSignals) { self.begin_frame(signals) } fn begin_frame(&mut self, signals: &InputSignals) {
fn any(&self) -> bool { self.any() } self.begin_frame(signals)
}
fn any(&self) -> bool {
self.any()
}
fn up(&self) -> &Button { &self.up } fn up(&self) -> &Button {
fn down(&self) -> &Button { &self.down } &self.up
fn left(&self) -> &Button { &self.left } }
fn right(&self) -> &Button { &self.right } fn down(&self) -> &Button {
fn a(&self) -> &Button { &self.a } &self.down
fn b(&self) -> &Button { &self.b } }
fn x(&self) -> &Button { &self.x } fn left(&self) -> &Button {
fn y(&self) -> &Button { &self.y } &self.left
fn l(&self) -> &Button { &self.l } }
fn r(&self) -> &Button { &self.r } fn right(&self) -> &Button {
fn start(&self) -> &Button { &self.start } &self.right
fn select(&self) -> &Button { &self.select } }
fn a(&self) -> &Button {
&self.a
}
fn b(&self) -> &Button {
&self.b
}
fn x(&self) -> &Button {
&self.x
}
fn y(&self) -> &Button {
&self.y
}
fn l(&self) -> &Button {
&self.l
}
fn r(&self) -> &Button {
&self.r
}
fn start(&self) -> &Button {
&self.start
}
fn select(&self) -> &Button {
&self.select
}
} }
impl Pad { impl Pad {
@ -66,5 +94,3 @@ impl Pad {
|| self.select.down || self.select.down
} }
} }

View File

@ -1,5 +1,5 @@
use prometeu_hal::{InputSignals, TouchBridge};
use prometeu_hal::button::Button; use prometeu_hal::button::Button;
use prometeu_hal::{InputSignals, TouchBridge};
#[derive(Default, Clone, Copy, Debug)] #[derive(Default, Clone, Copy, Debug)]
pub struct Touch { pub struct Touch {
@ -18,8 +18,16 @@ impl Touch {
} }
impl TouchBridge for Touch { impl TouchBridge for Touch {
fn begin_frame(&mut self, signals: &InputSignals) { self.begin_frame(signals) } fn begin_frame(&mut self, signals: &InputSignals) {
fn f(&self) -> &Button { &self.f } self.begin_frame(signals)
fn x(&self) -> i32 { self.x } }
fn y(&self) -> i32 { self.y } fn f(&self) -> &Button {
&self.f
}
fn x(&self) -> i32 {
self.x
}
fn y(&self) -> i32 {
self.y
}
} }

View File

@ -1,11 +1,11 @@
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::cartridge::Cartridge;
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine;
use crate::firmware::boot_target::BootTarget; use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep}; use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
use crate::firmware::prometeu_context::PrometeuContext; use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine;
/// PROMETEU Firmware. /// PROMETEU Firmware.
/// ///
@ -76,7 +76,12 @@ impl Firmware {
} }
/// Transitions the system to a new state, handling lifecycle hooks. /// Transitions the system to a new state, handling lifecycle hooks.
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) { pub fn change_state(
&mut self,
new_state: FirmwareState,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) {
self.on_exit(signals, hw); self.on_exit(signals, hw);
self.state = new_state; self.state = new_state;
self.state_initialized = false; self.state_initialized = false;
@ -109,7 +114,11 @@ impl Firmware {
/// Dispatches the `on_update` event to the current state implementation. /// Dispatches the `on_update` event to the current state implementation.
/// Returns an optional `FirmwareState` if a transition is requested. /// Returns an optional `FirmwareState` if a transition is requested.
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> { fn on_update(
&mut self,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) -> Option<FirmwareState> {
let mut req = PrometeuContext { let mut req = PrometeuContext {
vm: &mut self.vm, vm: &mut self.vm,
os: &mut self.os, os: &mut self.os,

View File

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

View File

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

View File

@ -1,8 +1,8 @@
use crate::firmware::boot_target::BootTarget; use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, HubHomeStep, LoadCartridgeStep}; use crate::firmware::firmware_state::{FirmwareState, HubHomeStep, LoadCartridgeStep};
use crate::firmware::prometeu_context::PrometeuContext; use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::cartridge_loader::CartridgeLoader; use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LaunchHubStep; pub struct LaunchHubStep;
@ -22,7 +22,12 @@ impl LaunchHubStep {
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge })); return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge }));
} }
Err(e) => { Err(e) => {
ctx.os.log(LogLevel::Error, LogSource::Pos, 0, format!("Failed to auto-load cartridge: {:?}", e)); ctx.os.log(
LogLevel::Error,
LogSource::Pos,
0,
format!("Failed to auto-load cartridge: {:?}", e),
);
} }
} }
} }

View File

@ -1,8 +1,8 @@
use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep}; use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep};
use crate::firmware::prometeu_context::PrometeuContext; use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::cartridge::{AppMode, Cartridge}; use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect; use prometeu_hal::window::Rect;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -12,13 +12,18 @@ pub struct LoadCartridgeStep {
impl LoadCartridgeStep { impl LoadCartridgeStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title)); ctx.os.log(
LogLevel::Info,
LogSource::Pos,
0,
format!("Loading cartridge: {}", self.cartridge.title),
);
// Initialize Asset Manager // Initialize Asset Manager
ctx.hw.assets_mut().initialize_for_cartridge( ctx.hw.assets_mut().initialize_for_cartridge(
self.cartridge.asset_table.clone(), self.cartridge.asset_table.clone(),
self.cartridge.preload.clone(), self.cartridge.preload.clone(),
self.cartridge.assets.clone() self.cartridge.assets.clone(),
); );
ctx.os.initialize_vm(ctx.vm, &self.cartridge); ctx.os.initialize_vm(ctx.vm, &self.cartridge);
@ -29,7 +34,7 @@ impl LoadCartridgeStep {
let id = ctx.hub.window_manager.add_window( let id = ctx.hub.window_manager.add_window(
self.cartridge.title.clone(), self.cartridge.title.clone(),
Rect { x: 40, y: 20, w: 240, h: 140 }, Rect { x: 40, y: 20, w: 240, h: 140 },
Color::WHITE Color::WHITE,
); );
ctx.hub.window_manager.set_focus(id); ctx.hub.window_manager.set_focus(id);

View File

@ -1,7 +1,7 @@
use prometeu_hal::log::{LogLevel, LogSource};
use crate::firmware::boot_target::BootTarget; use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep, SplashScreenStep}; use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep, SplashScreenStep};
use crate::firmware::prometeu_context::PrometeuContext; use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ResetStep; pub struct ResetStep;

View File

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

View File

@ -1,14 +1,14 @@
mod boot_target;
mod firmware; mod firmware;
pub mod firmware_state; pub mod firmware_state;
mod boot_target;
pub(crate) mod firmware_step_crash_screen;
pub(crate) mod firmware_step_game_running;
pub(crate) mod firmware_step_hub_home;
pub(crate) mod firmware_step_launch_hub;
pub(crate) mod firmware_step_load_cartridge;
pub(crate) mod firmware_step_reset; pub(crate) mod firmware_step_reset;
pub(crate) mod firmware_step_splash_screen; pub(crate) mod firmware_step_splash_screen;
pub(crate) mod firmware_step_launch_hub;
pub(crate) mod firmware_step_hub_home;
pub(crate) mod firmware_step_load_cartridge;
pub(crate) mod firmware_step_game_running;
pub(crate) mod firmware_step_crash_screen;
mod prometeu_context; mod prometeu_context;
pub use boot_target::BootTarget; pub use boot_target::BootTarget;

View File

@ -1,7 +1,7 @@
use crate::firmware::boot_target::BootTarget;
use prometeu_hal::{HardwareBridge, InputSignals}; use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_system::{PrometeuHub, VirtualMachineRuntime}; use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine; use prometeu_vm::VirtualMachine;
use crate::firmware::boot_target::BootTarget;
pub struct PrometeuContext<'a> { pub struct PrometeuContext<'a> {
pub vm: &'a mut VirtualMachine, pub vm: &'a mut VirtualMachine,

View File

@ -65,16 +65,10 @@ pub struct SlotRef {
impl SlotRef { impl SlotRef {
pub fn gfx(index: usize) -> Self { pub fn gfx(index: usize) -> Self {
Self { Self { asset_type: BankType::TILES, index }
asset_type: BankType::TILES,
index,
}
} }
pub fn audio(index: usize) -> Self { pub fn audio(index: usize) -> Self {
Self { Self { asset_type: BankType::SOUNDS, index }
asset_type: BankType::SOUNDS,
index,
}
} }
} }

View File

@ -1,4 +1,6 @@
use crate::asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats}; use crate::asset::{
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
};
pub trait AssetBridge { pub trait AssetBridge {
fn initialize_for_cartridge( fn initialize_for_cartridge(

View File

@ -1,5 +1,5 @@
use std::sync::Arc;
use crate::sample::Sample; use crate::sample::Sample;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopMode { pub enum LoopMode {

View File

@ -1,6 +1,6 @@
use crate::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use crate::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
pub struct CartridgeLoader; pub struct CartridgeLoader;
@ -30,8 +30,10 @@ impl DirectoryCartridgeLoader {
return Err(CartridgeError::InvalidManifest); return Err(CartridgeError::InvalidManifest);
} }
let manifest_content = fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?; let manifest_content =
let manifest: CartridgeManifest = serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?; fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?;
let manifest: CartridgeManifest =
serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?;
// Additional validation as per requirements // Additional validation as per requirements
if manifest.magic != "PMTU" { if manifest.magic != "PMTU" {

View File

@ -72,5 +72,4 @@ impl Color {
let hex = r8 << 16 | g8 << 8 | b8; let hex = r8 << 16 | g8 << 8 | b8;
hex as i32 hex as i32
} }
} }

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use prometeu_bytecode::Value;
use crate::cartridge::AppMode; use crate::cartridge::AppMode;
use prometeu_bytecode::Value;
use serde::{Deserialize, Serialize};
pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1; pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1;
@ -33,22 +33,11 @@ pub enum DebugCommand {
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum DebugResponse { pub enum DebugResponse {
#[serde(rename = "handshake")] #[serde(rename = "handshake")]
Handshake { Handshake { protocol_version: u32, runtime_version: String, cartridge: HandshakeCartridge },
protocol_version: u32,
runtime_version: String,
cartridge: HandshakeCartridge,
},
#[serde(rename = "getState")] #[serde(rename = "getState")]
GetState { GetState { pc: usize, stack_top: Vec<Value>, frame_index: u64, app_id: u32 },
pc: usize,
stack_top: Vec<Value>,
frame_index: u64,
app_id: u32,
},
#[serde(rename = "breakpoints")] #[serde(rename = "breakpoints")]
Breakpoints { Breakpoints { pcs: Vec<usize> },
pcs: Vec<usize>,
},
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -63,16 +52,9 @@ pub struct HandshakeCartridge {
#[serde(tag = "event")] #[serde(tag = "event")]
pub enum DebugEvent { pub enum DebugEvent {
#[serde(rename = "breakpointHit")] #[serde(rename = "breakpointHit")]
BreakpointHit { BreakpointHit { pc: usize, frame_index: u64 },
pc: usize,
frame_index: u64,
},
#[serde(rename = "log")] #[serde(rename = "log")]
Log { Log { level: String, source: String, msg: String },
level: String,
source: String,
msg: String,
},
#[serde(rename = "telemetry")] #[serde(rename = "telemetry")]
Telemetry { Telemetry {
frame_index: u64, frame_index: u64,
@ -90,12 +72,7 @@ pub enum DebugEvent {
audio_slots_occupied: u32, audio_slots_occupied: u32,
}, },
#[serde(rename = "cert")] #[serde(rename = "cert")]
Cert { Cert { rule: String, used: u64, limit: u64, frame_index: u64 },
rule: String,
used: u64,
limit: u64,
frame_index: u64,
},
} }
#[cfg(test)] #[cfg(test)]

View File

@ -25,7 +25,15 @@ pub trait GfxBridge {
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color); fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color);
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color); fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color);
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color); fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);
fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color); fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
);
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color); fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color); fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
fn present(&mut self); fn present(&mut self);

View File

@ -1,8 +1,8 @@
use prometeu_bytecode::{Value, TRAP_OOB};
use crate::vm_fault::VmFault; use crate::vm_fault::VmFault;
use prometeu_bytecode::{TRAP_OOB, Value};
pub struct HostReturn<'a> { pub struct HostReturn<'a> {
stack: &'a mut Vec<Value> stack: &'a mut Vec<Value>,
} }
impl<'a> HostReturn<'a> { impl<'a> HostReturn<'a> {

View File

@ -1,31 +1,31 @@
pub mod asset;
pub mod asset_bridge; pub mod asset_bridge;
pub mod audio_bridge; pub mod audio_bridge;
pub mod button;
pub mod cartridge;
pub mod cartridge_loader;
pub mod color;
pub mod debugger_protocol;
pub mod gfx_bridge; pub mod gfx_bridge;
pub mod hardware_bridge; pub mod hardware_bridge;
pub mod host_context; pub mod host_context;
pub mod host_return; pub mod host_return;
pub mod input_signals;
pub mod log;
pub mod native_helpers;
pub mod native_interface; pub mod native_interface;
pub mod pad_bridge; pub mod pad_bridge;
pub mod touch_bridge; pub mod sample;
pub mod native_helpers;
pub mod input_signals;
pub mod cartridge;
pub mod cartridge_loader;
pub mod debugger_protocol;
pub mod asset;
pub mod color;
pub mod button;
pub mod tile;
pub mod tile_layer;
pub mod tile_bank;
pub mod sound_bank; pub mod sound_bank;
pub mod sprite; pub mod sprite;
pub mod sample;
pub mod window;
pub mod syscalls; pub mod syscalls;
pub mod telemetry; pub mod telemetry;
pub mod log; pub mod tile;
pub mod tile_bank;
pub mod tile_layer;
pub mod touch_bridge;
pub mod vm_fault; pub mod vm_fault;
pub mod window;
pub use asset_bridge::AssetBridge; pub use asset_bridge::AssetBridge;
pub use audio_bridge::{AudioBridge, LoopMode}; pub use audio_bridge::{AudioBridge, LoopMode};
@ -34,7 +34,7 @@ pub use hardware_bridge::HardwareBridge;
pub use host_context::{HostContext, HostContextProvider}; pub use host_context::{HostContext, HostContextProvider};
pub use host_return::HostReturn; pub use host_return::HostReturn;
pub use input_signals::InputSignals; pub use input_signals::InputSignals;
pub use native_helpers::{expect_bool, expect_bounded, expect_int};
pub use native_interface::{NativeInterface, SyscallId}; pub use native_interface::{NativeInterface, SyscallId};
pub use pad_bridge::PadBridge; pub use pad_bridge::PadBridge;
pub use touch_bridge::TouchBridge; pub use touch_bridge::TouchBridge;
pub use native_helpers::{expect_bool, expect_bounded, expect_int};

View File

@ -1,5 +1,5 @@
use std::collections::VecDeque;
use crate::log::{LogEvent, LogLevel, LogSource}; use crate::log::{LogEvent, LogLevel, LogSource};
use std::collections::VecDeque;
pub struct LogService { pub struct LogService {
events: VecDeque<LogEvent>, events: VecDeque<LogEvent>,
@ -9,14 +9,18 @@ pub struct LogService {
impl LogService { impl LogService {
pub fn new(capacity: usize) -> Self { pub fn new(capacity: usize) -> Self {
Self { Self { events: VecDeque::with_capacity(capacity), capacity, next_seq: 0 }
events: VecDeque::with_capacity(capacity),
capacity,
next_seq: 0,
}
} }
pub fn log(&mut self, ts_ms: u64, frame: u64, level: LogLevel, source: LogSource, tag: u16, msg: String) { pub fn log(
&mut self,
ts_ms: u64,
frame: u64,
level: LogLevel,
source: LogSource,
tag: u16,
msg: String,
) {
if self.events.len() >= self.capacity { if self.events.len() >= self.capacity {
self.events.pop_front(); self.events.pop_front();
} }

View File

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

View File

@ -1,5 +1,5 @@
use prometeu_bytecode::{Value, TRAP_TYPE};
use crate::vm_fault::VmFault; use crate::vm_fault::VmFault;
use prometeu_bytecode::{TRAP_TYPE, Value};
pub fn expect_bounded(args: &[Value], idx: usize) -> Result<u32, VmFault> { pub fn expect_bounded(args: &[Value], idx: usize) -> Result<u32, VmFault> {
args.get(idx) args.get(idx)

View File

@ -1,7 +1,7 @@
use prometeu_bytecode::Value;
use crate::host_context::HostContext; use crate::host_context::HostContext;
use crate::host_return::HostReturn; use crate::host_return::HostReturn;
use crate::vm_fault::VmFault; use crate::vm_fault::VmFault;
use prometeu_bytecode::Value;
pub type SyscallId = u32; pub type SyscallId = u32;
@ -11,5 +11,11 @@ pub trait NativeInterface {
/// ABI Rule: Arguments for the syscall are passed in `args`. /// ABI Rule: Arguments for the syscall are passed in `args`.
/// ///
/// Returns are written via `ret`. /// Returns are written via `ret`.
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext) -> Result<(), VmFault>; fn syscall(
&mut self,
id: SyscallId,
args: &[Value],
ret: &mut HostReturn,
ctx: &mut HostContext,
) -> Result<(), VmFault>;
} }

View File

@ -7,12 +7,7 @@ pub struct Sample {
impl Sample { impl Sample {
pub fn new(sample_rate: u32, data: Vec<i16>) -> Self { pub fn new(sample_rate: u32, data: Vec<i16>) -> Self {
Self { Self { sample_rate, data, loop_start: None, loop_end: None }
sample_rate,
data,
loop_start: None,
loop_end: None,
}
} }
pub fn with_loop(mut self, start: u32, end: u32) -> Self { pub fn with_loop(mut self, start: u32, end: u32) -> Self {

View File

@ -1,5 +1,5 @@
use std::sync::Arc;
use crate::sample::Sample; use crate::sample::Sample;
use std::sync::Arc;
/// A container for audio assets. /// A container for audio assets.
/// ///

View File

@ -38,7 +38,12 @@ impl Certifier {
Self { config } Self { config }
} }
pub fn evaluate(&self, telemetry: &TelemetryFrame, log_service: &mut LogService, ts_ms: u64) -> usize { pub fn evaluate(
&self,
telemetry: &TelemetryFrame,
log_service: &mut LogService,
ts_ms: u64,
) -> usize {
if !self.config.enabled { if !self.config.enabled {
return 0; return 0;
} }
@ -53,7 +58,10 @@ impl Certifier {
LogLevel::Warn, LogLevel::Warn,
LogSource::Pos, LogSource::Pos,
0xCA01, 0xCA01,
format!("Cert: cycles_used exceeded budget ({} > {})", telemetry.cycles_used, budget), format!(
"Cert: cycles_used exceeded budget ({} > {})",
telemetry.cycles_used, budget
),
); );
violations += 1; violations += 1;
} }
@ -67,7 +75,10 @@ impl Certifier {
LogLevel::Warn, LogLevel::Warn,
LogSource::Pos, LogSource::Pos,
0xCA02, 0xCA02,
format!("Cert: syscalls per frame exceeded limit ({} > {})", telemetry.syscalls, limit), format!(
"Cert: syscalls per frame exceeded limit ({} > {})",
telemetry.syscalls, limit
),
); );
violations += 1; violations += 1;
} }
@ -81,7 +92,10 @@ impl Certifier {
LogLevel::Warn, LogLevel::Warn,
LogSource::Pos, LogSource::Pos,
0xCA03, 0xCA03,
format!("Cert: host_cpu_time_us exceeded limit ({} > {})", telemetry.host_cpu_time_us, limit), format!(
"Cert: host_cpu_time_us exceeded limit ({} > {})",
telemetry.host_cpu_time_us, limit
),
); );
violations += 1; violations += 1;
} }

View File

@ -10,11 +10,7 @@ pub struct TileMap {
impl TileMap { impl TileMap {
fn create(width: usize, height: usize) -> Self { fn create(width: usize, height: usize) -> Self {
Self { Self { width, height, tiles: vec![Tile::default(); width * height] }
width,
height,
tiles: vec![Tile::default(); width * height],
}
} }
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) { pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
@ -24,7 +20,6 @@ impl TileMap {
} }
} }
pub struct TileLayer { pub struct TileLayer {
pub bank_id: u8, pub bank_id: u8,
pub tile_size: TileSize, pub tile_size: TileSize,
@ -33,11 +28,7 @@ pub struct TileLayer {
impl TileLayer { impl TileLayer {
fn create(width: usize, height: usize, tile_size: TileSize) -> Self { fn create(width: usize, height: usize, tile_size: TileSize) -> Self {
Self { Self { bank_id: 0, tile_size, map: TileMap::create(width, height) }
bank_id: 0,
tile_size,
map: TileMap::create(width, height),
}
} }
} }
@ -62,11 +53,7 @@ pub struct ScrollableTileLayer {
impl ScrollableTileLayer { impl ScrollableTileLayer {
pub fn new(width: usize, height: usize, tile_size: TileSize) -> Self { pub fn new(width: usize, height: usize, tile_size: TileSize) -> Self {
Self { Self { layer: TileLayer::create(width, height, tile_size), scroll_x: 0, scroll_y: 0 }
layer: TileLayer::create(width, height, tile_size),
scroll_x: 0,
scroll_y: 0,
}
} }
} }
@ -89,9 +76,7 @@ pub struct HudTileLayer {
impl HudTileLayer { impl HudTileLayer {
pub fn new(width: usize, height: usize) -> Self { pub fn new(width: usize, height: usize) -> Self {
Self { Self { layer: TileLayer::create(width, height, Size8) }
layer: TileLayer::create(width, height, Size8),
}
} }
} }

View File

@ -1,5 +1,5 @@
use crate::input_signals::InputSignals;
use crate::button::Button; use crate::button::Button;
use crate::input_signals::InputSignals;
pub trait TouchBridge { pub trait TouchBridge {
fn begin_frame(&mut self, signals: &InputSignals); fn begin_frame(&mut self, signals: &InputSignals);

View File

@ -1,6 +1,6 @@
mod virtual_machine_runtime;
mod services;
mod programs; mod programs;
mod services;
mod virtual_machine_runtime;
pub use programs::PrometeuHub; pub use programs::PrometeuHub;
pub use services::fs; pub use services::fs;

View File

@ -1,9 +1,9 @@
use crate::programs::prometeu_hub::window_manager::WindowManager;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::color::Color;
use prometeu_hal::HardwareBridge;
use prometeu_hal::window::Rect;
use crate::VirtualMachineRuntime; use crate::VirtualMachineRuntime;
use crate::programs::prometeu_hub::window_manager::WindowManager;
use prometeu_hal::HardwareBridge;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect;
/// PrometeuHub: Launcher and system UI environment. /// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub { pub struct PrometeuHub {
@ -12,9 +12,7 @@ pub struct PrometeuHub {
impl PrometeuHub { impl PrometeuHub {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { window_manager: WindowManager::new() }
window_manager: WindowManager::new(),
}
} }
pub fn init(&mut self) { pub fn init(&mut self) {
@ -28,16 +26,29 @@ impl PrometeuHub {
if hw.pad().a().pressed { if hw.pad().a().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string()); os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
next_window = Some(("Green Window".to_string(), Rect { x: 0, y: 0, w: 160, h: 90 }, Color::GREEN)); next_window = Some((
"Green Window".to_string(),
Rect { x: 0, y: 0, w: 160, h: 90 },
Color::GREEN,
));
} else if hw.pad().b().pressed { } else if hw.pad().b().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string()); os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
next_window = Some(("Indigo Window".to_string(), Rect { x: 160, y: 0, w: 160, h: 90 }, Color::INDIGO)); next_window = Some((
"Indigo Window".to_string(),
Rect { x: 160, y: 0, w: 160, h: 90 },
Color::INDIGO,
));
} else if hw.pad().x().pressed { } else if hw.pad().x().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string()); os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
next_window = Some(("Yellow Window".to_string(), Rect { x: 0, y: 90, w: 160, h: 90 }, Color::YELLOW)); next_window = Some((
"Yellow Window".to_string(),
Rect { x: 0, y: 90, w: 160, h: 90 },
Color::YELLOW,
));
} else if hw.pad().y().pressed { } else if hw.pad().y().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string()); os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
next_window = Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED)); next_window =
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
} }
if let Some((title, rect, color)) = next_window { if let Some((title, rect, color)) = next_window {

View File

@ -9,21 +9,12 @@ pub struct WindowManager {
impl WindowManager { impl WindowManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { windows: Vec::new(), focused: None }
windows: Vec::new(),
focused: None,
}
} }
pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId { pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId {
let id = WindowId(self.windows.len() as u32); let id = WindowId(self.windows.len() as u32);
let window = Window { let window = Window { id, viewport, has_focus: false, title, color };
id,
viewport,
has_focus: false,
title,
color,
};
self.windows.push(window); self.windows.push(window);
id id
} }
@ -55,8 +46,13 @@ mod tests {
#[test] #[test]
fn test_window_manager_focus() { fn test_window_manager_focus() {
let mut wm = WindowManager::new(); let mut wm = WindowManager::new();
let id1 = wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE); let id1 =
let id2 = wm.add_window("Window 2".to_string(), Rect { x: 10, y: 10, w: 10, h: 10 }, Color::WHITE); wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
let id2 = wm.add_window(
"Window 2".to_string(),
Rect { x: 10, y: 10, w: 10, h: 10 },
Color::WHITE,
);
assert_eq!(wm.windows.len(), 2); assert_eq!(wm.windows.len(), 2);
assert_eq!(wm.focused, None); assert_eq!(wm.focused, None);
@ -75,7 +71,8 @@ mod tests {
#[test] #[test]
fn test_window_manager_remove_window() { fn test_window_manager_remove_window() {
let mut wm = WindowManager::new(); let mut wm = WindowManager::new();
let id = wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE); let id =
wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
wm.set_focus(id); wm.set_focus(id);
assert_eq!(wm.focused, Some(id)); assert_eq!(wm.focused, Some(id));

View File

@ -8,5 +8,7 @@ pub trait FsBackend: Send + Sync {
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError>; fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError>;
fn delete(&mut self, path: &str) -> Result<(), FsError>; fn delete(&mut self, path: &str) -> Result<(), FsError>;
fn exists(&self, path: &str) -> bool; fn exists(&self, path: &str) -> bool;
fn is_healthy(&self) -> bool { true } fn is_healthy(&self) -> bool {
true
}
} }

View File

@ -1,7 +1,7 @@
mod fs_backend;
mod fs_entry;
mod fs_error; mod fs_error;
mod fs_state; mod fs_state;
mod fs_entry;
mod fs_backend;
mod virtual_fs; mod virtual_fs;
pub use fs_backend::FsBackend; pub use fs_backend::FsBackend;

View File

@ -88,22 +88,21 @@ mod tests {
impl MockBackend { impl MockBackend {
fn new() -> Self { fn new() -> Self {
Self { Self { files: HashMap::new(), healthy: true }
files: HashMap::new(),
healthy: true,
}
} }
} }
impl FsBackend for MockBackend { impl FsBackend for MockBackend {
fn mount(&mut self) -> Result<(), FsError> { Ok(()) } fn mount(&mut self) -> Result<(), FsError> {
Ok(())
}
fn unmount(&mut self) {} fn unmount(&mut self) {}
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> { fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
Ok(self.files.keys().map(|name| FsEntry { Ok(self
name: name.clone(), .files
is_dir: false, .keys()
size: 0, .map(|name| FsEntry { name: name.clone(), is_dir: false, size: 0 })
}).collect()) .collect())
} }
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> { fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
self.files.get(path).cloned().ok_or(FsError::NotFound) self.files.get(path).cloned().ok_or(FsError::NotFound)

View File

@ -1,21 +1,20 @@
use prometeu_hal::syscalls::Syscall;
use crate::fs::{FsBackend, FsState, VirtualFS}; use crate::fs::{FsBackend, FsState, VirtualFS};
use prometeu_hal::{HardwareBridge, InputSignals}; use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
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, 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::asset::{BankType, LoadStatus, SlotRef};
use prometeu_hal::button::Button; use prometeu_hal::button::Button;
use prometeu_hal::cartridge::{AppMode, Cartridge}; use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::sprite::Sprite; use prometeu_hal::sprite::Sprite;
use prometeu_hal::syscalls::Syscall;
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use prometeu_hal::tile::Tile; use prometeu_hal::tile::Tile;
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int};
use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine}; use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
use std::collections::HashMap;
use std::time::Instant;
pub struct VirtualMachineRuntime { pub struct VirtualMachineRuntime {
/// Host Tick Index: Incremented on every host hardware update (usually 60Hz). /// Host Tick Index: Incremented on every host hardware update (usually 60Hz).
@ -128,7 +127,12 @@ impl VirtualMachineRuntime {
match self.fs.mount(backend) { match self.fs.mount(backend) {
Ok(_) => { Ok(_) => {
self.fs_state = FsState::Mounted; self.fs_state = FsState::Mounted;
self.log(LogLevel::Info, LogSource::Fs, 0, "Filesystem mounted successfully".to_string()); self.log(
LogLevel::Info,
LogSource::Fs,
0,
"Filesystem mounted successfully".to_string(),
);
} }
Err(e) => { Err(e) => {
let err_msg = format!("Failed to mount filesystem: {:?}", e); let err_msg = format!("Failed to mount filesystem: {:?}", e);
@ -146,7 +150,12 @@ impl VirtualMachineRuntime {
fn update_fs(&mut self) { fn update_fs(&mut self) {
if self.fs_state == FsState::Mounted { if self.fs_state == FsState::Mounted {
if !self.fs.is_healthy() { if !self.fs.is_healthy() {
self.log(LogLevel::Error, LogSource::Fs, 0, "Filesystem became unhealthy, unmounting".to_string()); self.log(
LogLevel::Error,
LogSource::Fs,
0,
"Filesystem became unhealthy, unmounting".to_string(),
);
self.unmount_fs(); self.unmount_fs();
} }
} }
@ -173,7 +182,12 @@ impl VirtualMachineRuntime {
self.current_entrypoint = cartridge.entrypoint.clone(); self.current_entrypoint = cartridge.entrypoint.clone();
} }
Err(e) => { Err(e) => {
self.log(LogLevel::Error, LogSource::Vm, 0, format!("Failed to initialize VM: {:?}", e)); self.log(
LogLevel::Error,
LogSource::Vm,
0,
format!("Failed to initialize VM: {:?}", e),
);
// Fail fast: no program is installed, no app id is switched. // Fail fast: no program is installed, no app id is switched.
// We don't update current_app_id or other fields. // We don't update current_app_id or other fields.
} }
@ -181,7 +195,11 @@ impl VirtualMachineRuntime {
} }
/// Executes a single VM instruction (Debug). /// Executes a single VM instruction (Debug).
pub fn debug_step_instruction(&mut self, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Option<String> { pub fn debug_step_instruction(
&mut self,
vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge,
) -> Option<String> {
let mut ctx = HostContext::new(Some(hw)); let mut ctx = HostContext::new(Some(hw));
match vm.step(self, &mut ctx) { match vm.step(self, &mut ctx) {
Ok(_) => None, Ok(_) => None,
@ -198,7 +216,12 @@ impl VirtualMachineRuntime {
/// This method is responsible for managing the logical frame lifecycle. /// This method is responsible for managing the logical frame lifecycle.
/// A single host tick might execute a full logical frame, part of it, /// A single host tick might execute a full logical frame, part of it,
/// or multiple frames depending on the configured slices. /// or multiple frames depending on the configured slices.
pub fn tick(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> { pub fn tick(
&mut self,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) -> Option<String> {
let start = Instant::now(); let start = Instant::now();
self.tick_index += 1; self.tick_index += 1;
@ -228,7 +251,11 @@ impl VirtualMachineRuntime {
// Reset telemetry for the new logical frame // Reset telemetry for the new logical frame
self.telemetry_current = TelemetryFrame { self.telemetry_current = TelemetryFrame {
frame_index: self.logical_frame_index, frame_index: self.logical_frame_index,
cycles_budget: self.certifier.config.cycles_budget_per_frame.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME), cycles_budget: self
.certifier
.config
.cycles_budget_per_frame
.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
..Default::default() ..Default::default()
}; };
} }
@ -247,7 +274,8 @@ impl VirtualMachineRuntime {
match run_result { match run_result {
Ok(run) => { Ok(run) => {
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used); self.logical_frame_remaining_cycles =
self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
// Accumulate metrics for telemetry and certification // Accumulate metrics for telemetry and certification
self.telemetry_current.cycles_used += run.cycles_used; self.telemetry_current.cycles_used += run.cycles_used;
@ -257,7 +285,12 @@ impl VirtualMachineRuntime {
if run.reason == LogicalFrameEndingReason::Breakpoint { if run.reason == LogicalFrameEndingReason::Breakpoint {
self.paused = true; self.paused = true;
self.debug_step_request = false; self.debug_step_request = false;
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc)); self.log(
LogLevel::Info,
LogSource::Vm,
0xDEB1,
format!("Breakpoint hit at PC 0x{:X}", vm.pc),
);
} }
// Handle Panics // Handle Panics
@ -268,18 +301,24 @@ impl VirtualMachineRuntime {
} }
// 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned) // 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned)
if run.reason == LogicalFrameEndingReason::FrameSync || if run.reason == LogicalFrameEndingReason::FrameSync
run.reason == LogicalFrameEndingReason::EndOfRom { || run.reason == LogicalFrameEndingReason::EndOfRom
{
// All drawing commands for this frame are now complete. // All drawing commands for this frame are now complete.
// Finalize the framebuffer. // Finalize the framebuffer.
hw.gfx_mut().render_all(); hw.gfx_mut().render_all();
// Finalize frame telemetry // Finalize frame telemetry
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64; self.telemetry_current.host_cpu_time_us =
start.elapsed().as_micros() as u64;
// Evaluate CAP (Execution Budget Compliance) // Evaluate CAP (Execution Budget Compliance)
let ts_ms = self.boot_time.elapsed().as_millis() as u64; let ts_ms = self.boot_time.elapsed().as_millis() as u64;
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32; self.telemetry_current.violations = self.certifier.evaluate(
&self.telemetry_current,
&mut self.log_service,
ts_ms,
) as u32;
// Latch telemetry for the Host/Debugger to read. // Latch telemetry for the Host/Debugger to read.
self.telemetry_last = self.telemetry_current; self.telemetry_last = self.telemetry_current;
@ -324,7 +363,9 @@ impl VirtualMachineRuntime {
self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32; self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32;
// If the frame ended exactly in this tick, we update the final real time in the latch. // If the frame ended exactly in this tick, we update the final real time in the latch.
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) { if !self.logical_frame_active
&& self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1)
{
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us; self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget; self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget;
self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes; self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes;
@ -343,7 +384,6 @@ impl VirtualMachineRuntime {
self.logs_written_this_frame.clear(); self.logs_written_this_frame.clear();
} }
// Helper for syscalls // Helper for syscalls
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> { fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
let level = match level_val { let level = match level_val {
@ -361,7 +401,12 @@ impl VirtualMachineRuntime {
if count >= Self::MAX_LOGS_PER_FRAME { if count >= Self::MAX_LOGS_PER_FRAME {
if count == Self::MAX_LOGS_PER_FRAME { if count == Self::MAX_LOGS_PER_FRAME {
self.logs_written_this_frame.insert(app_id, count + 1); self.logs_written_this_frame.insert(app_id, count + 1);
self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string()); self.log(
LogLevel::Warn,
LogSource::App { app_id },
0,
"App exceeded log limit per frame".to_string(),
);
} }
return Ok(()); return Ok(());
} }
@ -434,11 +479,17 @@ impl NativeInterface for VirtualMachineRuntime {
/// - 0x5000: Logging /// - 0x5000: Logging
/// ///
/// Each syscall returns the number of virtual cycles it consumed. /// 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> { fn syscall(
&mut self,
id: SyscallId,
args: &[Value],
ret: &mut HostReturn,
ctx: &mut HostContext,
) -> Result<(), VmFault> {
self.telemetry_current.syscalls += 1; self.telemetry_current.syscalls += 1;
let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(TRAP_INVALID_SYSCALL, format!( let syscall = Syscall::from_u32(id).ok_or_else(|| {
"Unknown syscall: 0x{:08X}", id VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
)))?; })?;
// Handle hardware-less syscalls first // Handle hardware-less syscalls first
match syscall { match syscall {
@ -457,7 +508,6 @@ impl NativeInterface for VirtualMachineRuntime {
match syscall { match syscall {
// --- System Syscalls --- // --- System Syscalls ---
Syscall::SystemHasCart => unreachable!(), Syscall::SystemHasCart => unreachable!(),
Syscall::SystemRunCart => unreachable!(), Syscall::SystemRunCart => unreachable!(),
@ -535,7 +585,10 @@ impl NativeInterface for VirtualMachineRuntime {
} }
// gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority) // gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority)
Syscall::GfxSetSprite => { Syscall::GfxSetSprite => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? { let asset_name = match args
.get(0)
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
{
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
}; };
@ -549,7 +602,8 @@ impl NativeInterface for VirtualMachineRuntime {
let flip_y = expect_bool(args, 8)?; let flip_y = expect_bool(args, 8)?;
let priority = expect_int(args, 9)? as u8; let priority = expect_int(args, 9)? as u8;
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0); let bank_id =
hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
if index < 512 { if index < 512 {
*hw.gfx_mut().sprite_mut(index) = Sprite { *hw.gfx_mut().sprite_mut(index) = Sprite {
@ -569,7 +623,10 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::GfxDrawText => { Syscall::GfxDrawText => {
let x = expect_int(args, 0)? as i32; let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32; let y = expect_int(args, 1)? as i32;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? { let msg = match args
.get(2)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
}; };
@ -583,7 +640,10 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::GfxClear565 => { Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32; let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF { if color_val > 0xFFFF {
return Err(VmFault::Trap(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); let color = Color::from_raw(color_val as u16);
hw.gfx_mut().clear(color); hw.gfx_mut().clear(color);
@ -655,9 +715,18 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::InputPadSnapshot => { Syscall::InputPadSnapshot => {
let pad = hw.pad(); let pad = hw.pad();
for btn in [ for btn in [
pad.up(), pad.down(), pad.left(), pad.right(), pad.up(),
pad.a(), pad.b(), pad.x(), pad.y(), pad.down(),
pad.l(), pad.r(), pad.start(), pad.select(), pad.left(),
pad.right(),
pad.a(),
pad.b(),
pad.x(),
pad.y(),
pad.l(),
pad.r(),
pad.start(),
pad.select(),
] { ] {
ret.push_bool(btn.pressed); ret.push_bool(btn.pressed);
ret.push_bool(btn.released); ret.push_bool(btn.released);
@ -783,7 +852,10 @@ impl NativeInterface for VirtualMachineRuntime {
let voice_id = expect_int(args, 1)? as usize; let voice_id = expect_int(args, 1)? as usize;
let volume = expect_int(args, 2)? as u8; let volume = expect_int(args, 2)? as u8;
let pan = expect_int(args, 3)? as u8; let pan = expect_int(args, 3)? as u8;
let pitch = match args.get(4).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? { let pitch = match args
.get(4)
.ok_or_else(|| VmFault::Panic("Missing pitch".into()))?
{
Value::Float(f) => *f, Value::Float(f) => *f,
Value::Int32(i) => *i as f64, Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64, Value::Int64(i) => *i as f64,
@ -791,14 +863,26 @@ impl NativeInterface for VirtualMachineRuntime {
_ => return Err(VmFault::Trap(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); hw.audio_mut().play(
0,
sample_id as u16,
voice_id,
volume,
pan,
pitch,
0,
prometeu_hal::LoopMode::Off,
);
ret.push_null(); ret.push_null();
Ok(()) Ok(())
} }
// audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode) // audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode)
Syscall::AudioPlay => { Syscall::AudioPlay => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? { let asset_name = match args
.get(0)
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
{
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
}; };
@ -806,7 +890,10 @@ impl NativeInterface for VirtualMachineRuntime {
let voice_id = expect_int(args, 2)? as usize; let voice_id = expect_int(args, 2)? as usize;
let volume = expect_int(args, 3)? as u8; let volume = expect_int(args, 3)? as u8;
let pan = expect_int(args, 4)? as u8; let pan = expect_int(args, 4)? as u8;
let pitch = match args.get(5).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? { let pitch = match args
.get(5)
.ok_or_else(|| VmFault::Panic("Missing pitch".into()))?
{
Value::Float(f) => *f, Value::Float(f) => *f,
Value::Int32(i) => *i as f64, Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64, Value::Int64(i) => *i as f64,
@ -818,7 +905,8 @@ impl NativeInterface for VirtualMachineRuntime {
_ => prometeu_hal::LoopMode::On, _ => prometeu_hal::LoopMode::On,
}; };
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0); let bank_id =
hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0);
hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode); hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode);
ret.push_null(); ret.push_null();
@ -846,7 +934,10 @@ impl NativeInterface for VirtualMachineRuntime {
// FS_READ(handle) -> content // FS_READ(handle) -> content
Syscall::FsRead => { Syscall::FsRead => {
let handle = expect_int(args, 0)? as u32; let handle = expect_int(args, 0)? as u32;
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?; let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.read_file(path) { match self.fs.read_file(path) {
Ok(data) => { Ok(data) => {
let s = String::from_utf8_lossy(&data).into_owned(); let s = String::from_utf8_lossy(&data).into_owned();
@ -859,11 +950,17 @@ impl NativeInterface for VirtualMachineRuntime {
// FS_WRITE(handle, content) // FS_WRITE(handle, content)
Syscall::FsWrite => { Syscall::FsWrite => {
let handle = expect_int(args, 0)? as u32; let handle = expect_int(args, 0)? as u32;
let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? { let content = match args
.get(1)
.ok_or_else(|| VmFault::Panic("Missing content".into()))?
{
Value::String(s) => s.as_bytes().to_vec(), Value::String(s) => s.as_bytes().to_vec(),
_ => return Err(VmFault::Trap(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()))?; let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.write_file(path, &content) { match self.fs.write_file(path, &content) {
Ok(_) => ret.push_bool(true), Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false), Err(_) => ret.push_bool(false),
@ -919,7 +1016,10 @@ impl NativeInterface for VirtualMachineRuntime {
// LOG_WRITE(level, msg) // LOG_WRITE(level, msg)
Syscall::LogWrite => { Syscall::LogWrite => {
let level = expect_int(args, 0)?; let level = expect_int(args, 0)?;
let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? { let msg = match args
.get(1)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
}; };
@ -931,7 +1031,10 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::LogWriteTag => { Syscall::LogWriteTag => {
let level = expect_int(args, 0)?; let level = expect_int(args, 0)?;
let tag = expect_int(args, 1)? as u16; let tag = expect_int(args, 1)? as u16;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? { let msg = match args
.get(2)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
}; };
@ -942,7 +1045,10 @@ impl NativeInterface for VirtualMachineRuntime {
// --- Asset Syscalls --- // --- Asset Syscalls ---
Syscall::AssetLoad => { Syscall::AssetLoad => {
let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? { let asset_id = match args
.get(0)
.ok_or_else(|| VmFault::Panic("Missing asset_id".into()))?
{
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_id".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_id".into())),
}; };

View File

@ -7,3 +7,6 @@ license.workspace = true
[dependencies] [dependencies]
prometeu-bytecode = { path = "../prometeu-bytecode" } prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-hal = { path = "../prometeu-hal" } prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]
prometeu-test-support = { path = "../../dev/prometeu-test-support" }

View File

@ -1,11 +1,9 @@
mod virtual_machine;
mod call_frame; mod call_frame;
mod scope_frame;
pub mod local_addressing; pub mod local_addressing;
mod scope_frame;
pub mod verifier; pub mod verifier;
mod virtual_machine;
pub mod vm_init_error; pub mod vm_init_error;
pub use prometeu_hal::{ pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
HostContext, HostReturn, NativeInterface, SyscallId,
};
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine}; pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};

View File

@ -1,6 +1,6 @@
use crate::call_frame::CallFrame; use crate::call_frame::CallFrame;
use prometeu_bytecode::{TrapInfo, TRAP_INVALID_LOCAL};
use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::{TRAP_INVALID_LOCAL, TrapInfo};
/// Computes the absolute stack index for the start of the current frame's locals (including args). /// Computes the absolute stack index for the start of the current frame's locals (including args).
pub fn local_base(frame: &CallFrame) -> usize { pub fn local_base(frame: &CallFrame) -> usize {
@ -14,7 +14,12 @@ pub fn local_index(frame: &CallFrame, slot: u32) -> usize {
/// Validates that a local slot index is within the valid range for the function. /// Validates that a local slot index is within the valid range for the function.
/// Range: 0 <= slot < (param_slots + local_slots) /// Range: 0 <= slot < (param_slots + local_slots)
pub fn check_local_slot(meta: &FunctionMeta, slot: u32, opcode: u16, pc: u32) -> Result<(), TrapInfo> { pub fn check_local_slot(
meta: &FunctionMeta,
slot: u32,
opcode: u16,
pc: u32,
) -> Result<(), TrapInfo> {
let limit = meta.param_slots as u32 + meta.local_slots as u32; let limit = meta.param_slots as u32 + meta.local_slots as u32;
if slot < limit { if slot < limit {
Ok(()) Ok(())

View File

@ -1,9 +1,9 @@
use prometeu_hal::syscalls::Syscall; use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::{decode_next, DecodeError};
use prometeu_bytecode::OpCode; use prometeu_bytecode::OpCode;
use prometeu_bytecode::OpCodeSpecExt; use prometeu_bytecode::OpCodeSpecExt;
use prometeu_bytecode::FunctionMeta; use prometeu_bytecode::{DecodeError, decode_next};
use prometeu_bytecode::{compute_function_layouts, FunctionLayout}; use prometeu_bytecode::{FunctionLayout, compute_function_layouts};
use prometeu_hal::syscalls::Syscall;
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -71,14 +71,23 @@ impl Verifier {
while pc < func_code.len() { while pc < func_code.len() {
valid_pc.insert(pc); valid_pc.insert(pc);
let instr = decode_next(pc, func_code).map_err(|e| match e { let instr = decode_next(pc, func_code).map_err(|e| match e {
DecodeError::UnknownOpcode { pc: _, opcode } => DecodeError::UnknownOpcode { pc: _, opcode } => {
VerifierError::UnknownOpcode { pc: func_start + pc, opcode }, VerifierError::UnknownOpcode { pc: func_start + pc, opcode }
DecodeError::TruncatedOpcode { pc: _ } => }
VerifierError::TruncatedOpcode { pc: func_start + pc }, DecodeError::TruncatedOpcode { pc: _ } => {
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => VerifierError::TruncatedOpcode { pc: func_start + pc }
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have }, }
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => {
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need: expected, have: actual }, VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have }
}
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => {
VerifierError::TruncatedImmediate {
pc: func_start + pc,
opcode,
need: expected,
have: actual,
}
}
})?; })?;
pc = instr.next_pc; pc = instr.next_pc;
} }
@ -113,9 +122,7 @@ impl Verifier {
})?; })?;
(callee.param_slots, callee.return_slots) (callee.param_slots, callee.return_slots)
} }
OpCode::Ret => { OpCode::Ret => (func.return_slots, 0),
(func.return_slots, 0)
}
OpCode::Syscall => { OpCode::Syscall => {
let id = instr.imm_u32().unwrap(); let id = instr.imm_u32().unwrap();
let syscall = Syscall::from_u32(id).ok_or_else(|| { let syscall = Syscall::from_u32(id).ok_or_else(|| {
@ -127,7 +134,10 @@ impl Verifier {
}; };
if in_height < pops { if in_height < pops {
return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: instr.opcode }); return Err(VerifierError::StackUnderflow {
pc: func_start + pc,
opcode: instr.opcode,
});
} }
let out_height = in_height - pops + pushes; let out_height = in_height - pops + pushes;
@ -135,7 +145,11 @@ impl Verifier {
if instr.opcode == OpCode::Ret { if instr.opcode == OpCode::Ret {
if in_height != func.return_slots { if in_height != func.return_slots {
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: in_height, expected: func.return_slots }); return Err(VerifierError::BadRetStackHeight {
pc: func_start + pc,
height: in_height,
expected: func.return_slots,
});
} }
} }
@ -143,7 +157,8 @@ impl Verifier {
if spec.is_branch { if spec.is_branch {
// Canonical contract: branch immediate is RELATIVE to function start. // Canonical contract: branch immediate is RELATIVE to function start.
let target_rel = instr.imm_u32().unwrap() as usize; let target_rel = instr.imm_u32().unwrap() as usize;
let func_end_abs = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len()); let func_end_abs =
layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
let func_len = func_end_abs - func_start; let func_len = func_end_abs - func_start;
if target_rel > func_len { if target_rel > func_len {
@ -168,23 +183,38 @@ impl Verifier {
target_abs_expected, target_abs_expected,
is_boundary_target_rel is_boundary_target_rel
); );
return Err(VerifierError::InvalidJumpTarget { pc: pc_abs, target: target_abs_expected }); return Err(VerifierError::InvalidJumpTarget {
pc: pc_abs,
target: target_abs_expected,
});
} }
if target_rel == func_len { if target_rel == func_len {
// salto para o fim da função // salto para o fim da função
if out_height != func.return_slots { if out_height != func.return_slots {
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: out_height, expected: func.return_slots }); return Err(VerifierError::BadRetStackHeight {
pc: func_start + pc,
height: out_height,
expected: func.return_slots,
});
} }
// caminho termina aqui // caminho termina aqui
} else { } else {
if !valid_pc.contains(&target_rel) { if !valid_pc.contains(&target_rel) {
return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target_rel }); return Err(VerifierError::JumpToMidInstruction {
pc: func_start + pc,
target: func_start + target_rel,
});
} }
if let Some(&existing_height) = stack_height_in.get(&target_rel) { if let Some(&existing_height) = stack_height_in.get(&target_rel) {
if existing_height != out_height { if existing_height != out_height {
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target_rel, height_in: out_height, height_target: existing_height }); return Err(VerifierError::StackMismatchJoin {
pc: func_start + pc,
target: func_start + target_rel,
height_in: out_height,
height_target: existing_height,
});
} }
} else { } else {
stack_height_in.insert(target_rel, out_height); stack_height_in.insert(target_rel, out_height);
@ -199,7 +229,12 @@ impl Verifier {
if next_pc < func_len { if next_pc < func_len {
if let Some(&existing_height) = stack_height_in.get(&next_pc) { if let Some(&existing_height) = stack_height_in.get(&next_pc) {
if existing_height != out_height { if existing_height != out_height {
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + next_pc, height_in: out_height, height_target: existing_height }); return Err(VerifierError::StackMismatchJoin {
pc: func_start + pc,
target: func_start + next_pc,
height_in: out_height,
height_target: existing_height,
});
} }
} else { } else {
stack_height_in.insert(next_pc, out_height); stack_height_in.insert(next_pc, out_height);
@ -221,18 +256,15 @@ mod tests {
fn test_verifier_underflow() { fn test_verifier_underflow() {
// OpCode::Add (2 bytes) // OpCode::Add (2 bytes)
let code = vec![OpCode::Add as u8, 0x00]; let code = vec![OpCode::Add as u8, 0x00];
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
code_offset: 0,
code_len: 2,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add })); assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add }));
} }
#[test] #[test]
fn test_verifier_dup_underflow() { fn test_verifier_dup_underflow() {
let code = vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]]; let code =
vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }]; let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Dup })); assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Dup }));
@ -243,11 +275,7 @@ mod tests {
// Jmp (2 bytes) + 100u32 (4 bytes) // Jmp (2 bytes) + 100u32 (4 bytes)
let mut code = vec![OpCode::Jmp as u8, 0x00]; let mut code = vec![OpCode::Jmp as u8, 0x00];
code.extend_from_slice(&100u32.to_le_bytes()); code.extend_from_slice(&100u32.to_le_bytes());
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
code_offset: 0,
code_len: 6,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 })); assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 }));
} }
@ -262,11 +290,7 @@ mod tests {
code.push(0x00); code.push(0x00);
code.extend_from_slice(&1u32.to_le_bytes()); code.extend_from_slice(&1u32.to_le_bytes());
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 12, ..Default::default() }];
code_offset: 0,
code_len: 12,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 })); assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 }));
} }
@ -295,11 +319,7 @@ mod tests {
#[test] #[test]
fn test_verifier_truncation_opcode() { fn test_verifier_truncation_opcode() {
let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 1, ..Default::default() }];
code_offset: 0,
code_len: 1,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 })); assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 }));
} }
@ -308,13 +328,17 @@ mod tests {
fn test_verifier_truncation_immediate() { fn test_verifier_truncation_immediate() {
let mut code = vec![OpCode::PushI32 as u8, 0x00]; let mut code = vec![OpCode::PushI32 as u8, 0x00];
code.push(0x42); // Only 1 byte of 4-byte immediate code.push(0x42); // Only 1 byte of 4-byte immediate
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() }];
code_offset: 0,
code_len: 3,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::TruncatedImmediate { pc: 0, opcode: OpCode::PushI32, need: 4, have: 1 })); assert_eq!(
res,
Err(VerifierError::TruncatedImmediate {
pc: 0,
opcode: OpCode::PushI32,
need: 4,
have: 1
})
);
} }
#[test] #[test]
@ -328,24 +352,39 @@ mod tests {
// 27: Nop // 27: Nop
let mut code = Vec::new(); let mut code = Vec::new();
code.push(OpCode::PushBool as u8); code.push(0x00); code.push(1); // 0: PushBool (3 bytes) code.push(OpCode::PushBool as u8);
code.push(OpCode::JmpIfTrue as u8); code.push(0x00); code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes) code.push(0x00);
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes) code.push(1); // 0: PushBool (3 bytes)
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes) code.push(OpCode::JmpIfTrue as u8);
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes) code.push(0x00);
code.push(OpCode::Nop as u8); code.push(0x00); // 27: Nop (2 bytes) code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
code.push(OpCode::Jmp as u8);
code.push(0x00);
code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
code.push(OpCode::PushI32 as u8);
code.push(0x00);
code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
code.push(OpCode::Jmp as u8);
code.push(0x00);
code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
code.push(OpCode::Nop as u8);
code.push(0x00); // 27: Nop (2 bytes)
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 29, ..Default::default() }];
code_offset: 0,
code_len: 29,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
// Path 0->3->9->27: height 1-1+0 = 0. // Path 0->3->9->27: height 1-1+0 = 0.
// Path 0->3->15->21->27: height 1-1+1 = 1. // Path 0->3->15->21->27: height 1-1+1 = 1.
// Mismatch at 27: 0 vs 1. // Mismatch at 27: 0 vs 1.
assert_eq!(res, Err(VerifierError::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 })); assert_eq!(
res,
Err(VerifierError::StackMismatchJoin {
pc: 21,
target: 27,
height_in: 1,
height_target: 0
})
);
} }
#[test] #[test]
@ -374,10 +413,16 @@ mod tests {
// Add // Add
// Ret // Ret
let mut code = Vec::new(); let mut code = Vec::new();
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); code.push(OpCode::PushI32 as u8);
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes()); code.push(0x00);
code.push(OpCode::Add as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes());
code.push(OpCode::Ret as u8); code.push(0x00); code.push(OpCode::PushI32 as u8);
code.push(0x00);
code.extend_from_slice(&2u32.to_le_bytes());
code.push(OpCode::Add as u8);
code.push(0x00);
code.push(OpCode::Ret as u8);
code.push(0x00);
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta {
code_offset: 0, code_offset: 0,
@ -392,14 +437,11 @@ mod tests {
#[test] #[test]
fn test_verifier_invalid_syscall_id() { fn test_verifier_invalid_syscall_id() {
let mut code = Vec::new(); let mut code = Vec::new();
code.push(OpCode::Syscall as u8); code.push(0x00); code.push(OpCode::Syscall as u8);
code.push(0x00);
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID
let functions = vec![FunctionMeta { let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
code_offset: 0,
code_len: 6,
..Default::default()
}];
let res = Verifier::verify(&code, &functions); let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF })); assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
} }

View File

@ -3,10 +3,13 @@ use crate::scope_frame::ScopeFrame;
use crate::verifier::Verifier; use crate::verifier::Verifier;
use crate::vm_init_error::VmInitError; use crate::vm_init_error::VmInitError;
use crate::{HostContext, NativeInterface}; 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::OpCode;
use prometeu_bytecode::ProgramImage; use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value; use prometeu_bytecode::Value;
use prometeu_bytecode::{
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, TrapInfo,
};
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
/// Reason why the Virtual Machine stopped execution during a specific run. /// Reason why the Virtual Machine stopped execution during a specific run.
@ -126,7 +129,13 @@ impl VirtualMachine {
call_stack: Vec::new(), call_stack: Vec::new(),
scope_stack: Vec::new(), scope_stack: Vec::new(),
globals: Vec::new(), globals: Vec::new(),
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()), program: ProgramImage::new(
rom,
constant_pool,
vec![],
None,
std::collections::HashMap::new(),
),
heap: Vec::new(), heap: Vec::new(),
gate_pool: Vec::new(), gate_pool: Vec::new(),
cycles: 0, cycles: 0,
@ -137,7 +146,11 @@ impl VirtualMachine {
/// Resets the VM state and loads a new program. /// Resets the VM state and loads a new program.
/// This is typically called by the Firmware when starting a new App/Cartridge. /// This is typically called by the Firmware when starting a new App/Cartridge.
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) -> Result<(), VmInitError> { pub fn initialize(
&mut self,
program_bytes: Vec<u8>,
entrypoint: &str,
) -> Result<(), VmInitError> {
// Fail fast: reset state upfront. If we return early with an error, // Fail fast: reset state upfront. If we return early with an error,
// the VM is left in a "halted and empty" state. // the VM is left in a "halted and empty" state.
self.program = ProgramImage::default(); self.program = ProgramImage::default();
@ -169,7 +182,9 @@ impl VirtualMachine {
program program
} }
Err(prometeu_bytecode::LoadError::InvalidVersion) => return Err(VmInitError::UnsupportedFormat), Err(prometeu_bytecode::LoadError::InvalidVersion) => {
return Err(VmInitError::UnsupportedFormat);
}
Err(e) => { Err(e) => {
return Err(VmInitError::ImageLoadFailed(e)); return Err(VmInitError::ImageLoadFailed(e));
} }
@ -182,13 +197,17 @@ impl VirtualMachine {
let pc = if entrypoint.is_empty() { let pc = if entrypoint.is_empty() {
program.functions.get(0).map(|f| f.code_offset as usize).unwrap_or(0) program.functions.get(0).map(|f| f.code_offset as usize).unwrap_or(0)
} else if let Ok(func_idx) = entrypoint.parse::<usize>() { } else if let Ok(func_idx) = entrypoint.parse::<usize>() {
program.functions.get(func_idx) program
.functions
.get(func_idx)
.map(|f| f.code_offset as usize) .map(|f| f.code_offset as usize)
.ok_or(VmInitError::EntrypointNotFound)? .ok_or(VmInitError::EntrypointNotFound)?
} else { } else {
// Try to resolve as a symbol name from the exports map // Try to resolve as a symbol name from the exports map
if let Some(&func_idx) = program.exports.get(entrypoint) { if let Some(&func_idx) = program.exports.get(entrypoint) {
program.functions.get(func_idx as usize) program
.functions
.get(func_idx as usize)
.map(|f| f.code_offset as usize) .map(|f| f.code_offset as usize)
.ok_or(VmInitError::EntrypointNotFound)? .ok_or(VmInitError::EntrypointNotFound)?
} else { } else {
@ -211,9 +230,7 @@ impl VirtualMachine {
idx idx
} else { } else {
// Try to resolve as a symbol name // Try to resolve as a symbol name
self.program.exports.get(entrypoint) self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) // Default to 0 if not found
.map(|&idx| idx as usize)
.ok_or(()).unwrap_or(0) // Default to 0 if not found
}; };
let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default(); let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default();
@ -311,7 +328,10 @@ impl VirtualMachine {
// Integrity check: ensure real progress is being made to avoid infinite loops // Integrity check: ensure real progress is being made to avoid infinite loops
// caused by zero-cycle instructions or stuck PC. // caused by zero-cycle instructions or stuck PC.
if self.pc == pc_before && self.cycles == cycles_before && !self.halted { if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
ending_reason = Some(LogicalFrameEndingReason::Panic(format!("VM stuck at PC 0x{:08X}", self.pc))); ending_reason = Some(LogicalFrameEndingReason::Panic(format!(
"VM stuck at PC 0x{:08X}",
self.pc
)));
break; break;
} }
} }
@ -339,10 +359,7 @@ impl VirtualMachine {
if self.pc + 2 > self.program.rom.len() { if self.pc + 2 > self.program.rom.len() {
return Err("Unexpected end of ROM".into()); return Err("Unexpected end of ROM".into());
} }
let bytes = [ let bytes = [self.program.rom[self.pc], self.program.rom[self.pc + 1]];
self.program.rom[self.pc],
self.program.rom[self.pc + 1],
];
Ok(u16::from_le_bytes(bytes)) Ok(u16::from_le_bytes(bytes))
} }
@ -352,7 +369,11 @@ impl VirtualMachine {
/// 1. Fetch: Read the opcode from memory. /// 1. Fetch: Read the opcode from memory.
/// 2. Decode: Identify what operation to perform. /// 2. Decode: Identify what operation to perform.
/// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals. /// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals.
pub fn step(&mut self, native: &mut dyn NativeInterface, ctx: &mut HostContext) -> Result<(), LogicalFrameEndingReason> { pub fn step(
&mut self,
native: &mut dyn NativeInterface,
ctx: &mut HostContext,
) -> Result<(), LogicalFrameEndingReason> {
if self.halted || self.pc >= self.program.rom.len() { if self.halted || self.pc >= self.program.rom.len() {
return Ok(()); return Ok(());
} }
@ -373,35 +394,66 @@ impl VirtualMachine {
self.halted = true; self.halted = true;
} }
OpCode::Jmp => { OpCode::Jmp => {
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let target = instr
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0); .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let func_start = self
.call_stack
.last()
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
.unwrap_or(0);
self.pc = func_start + target; self.pc = func_start + target;
} }
OpCode::JmpIfFalse => { OpCode::JmpIfFalse => {
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let target = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
match val { match val {
Value::Boolean(false) => { Value::Boolean(false) => {
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0); let func_start = self
.call_stack
.last()
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
.unwrap_or(0);
self.pc = func_start + target; self.pc = func_start + target;
} }
Value::Boolean(true) => {} Value::Boolean(true) => {}
_ => { _ => {
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_FALSE, got {:?}", val), start_pc as u32)); return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!("Expected boolean for JMP_IF_FALSE, got {:?}", val),
start_pc as u32,
));
} }
} }
} }
OpCode::JmpIfTrue => { OpCode::JmpIfTrue => {
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let target = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
match val { match val {
Value::Boolean(true) => { Value::Boolean(true) => {
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0); let func_start = self
.call_stack
.last()
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
.unwrap_or(0);
self.pc = func_start + target; self.pc = func_start + target;
} }
Value::Boolean(false) => {} Value::Boolean(false) => {}
_ => { _ => {
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_TRUE, got {:?}", val), start_pc as u32)); return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!("Expected boolean for JMP_IF_TRUE, got {:?}", val),
start_pc as u32,
));
} }
} }
} }
@ -409,38 +461,60 @@ impl VirtualMachine {
// Handled in run_budget for interruption // Handled in run_budget for interruption
} }
OpCode::PushConst => { OpCode::PushConst => {
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let idx = instr
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid constant index".into()))?; .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Invalid constant index".into())
})?;
self.push(val); self.push(val);
} }
OpCode::PushI64 => { OpCode::PushI64 => {
let val = instr.imm_i64().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let val = instr
.imm_i64()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Int64(val)); self.push(Value::Int64(val));
} }
OpCode::PushI32 => { OpCode::PushI32 => {
let val = instr.imm_i32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let val = instr
.imm_i32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Int32(val)); self.push(Value::Int32(val));
} }
OpCode::PushBounded => { OpCode::PushBounded => {
let val = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let val = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
if val > 0xFFFF { if val > 0xFFFF {
return Err(self.trap(TRAP_OOB, opcode as u16, format!("Bounded value overflow: {} > 0xFFFF", val), start_pc as u32)); return Err(self.trap(
TRAP_OOB,
opcode as u16,
format!("Bounded value overflow: {} > 0xFFFF", val),
start_pc as u32,
));
} }
self.push(Value::Bounded(val)); self.push(Value::Bounded(val));
} }
OpCode::PushF64 => { OpCode::PushF64 => {
let val = instr.imm_f64().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let val = instr
.imm_f64()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Float(val)); self.push(Value::Float(val));
} }
OpCode::PushBool => { OpCode::PushBool => {
let val = instr.imm_u8().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let val = instr
.imm_u8()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Boolean(val != 0)); self.push(Value::Boolean(val != 0));
} }
OpCode::Pop => { OpCode::Pop => {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
} }
OpCode::PopN => { OpCode::PopN => {
let n = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let n = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
for _ in 0..n { for _ in 0..n {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
} }
@ -461,7 +535,9 @@ impl VirtualMachine {
} }
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(*b))), (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(*b))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((*a as i64).wrapping_add(*b))), (Value::Int32(a), Value::Int64(b)) => {
Ok(Value::Int64((*a as i64).wrapping_add(*b)))
}
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_add(*b as i64))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_add(*b as i64))),
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
(Value::Int32(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)), (Value::Int32(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
@ -471,7 +547,10 @@ impl VirtualMachine {
(Value::Bounded(a), Value::Bounded(b)) => { (Value::Bounded(a), Value::Bounded(b)) => {
let res = a.saturating_add(*b); let res = a.saturating_add(*b);
if res > 0xFFFF { if res > 0xFFFF {
Err(OpError::Trap(TRAP_OOB, format!("Bounded addition overflow: {} + {} = {}", a, b, res))) Err(OpError::Trap(
TRAP_OOB,
format!("Bounded addition overflow: {} + {} = {}", a, b, res),
))
} else { } else {
Ok(Value::Bounded(res)) Ok(Value::Bounded(res))
} }
@ -490,7 +569,10 @@ impl VirtualMachine {
(Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)), (Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)),
(Value::Bounded(a), Value::Bounded(b)) => { (Value::Bounded(a), Value::Bounded(b)) => {
if a < b { if a < b {
Err(OpError::Trap(TRAP_OOB, format!("Bounded subtraction underflow: {} - {} < 0", a, b))) Err(OpError::Trap(
TRAP_OOB,
format!("Bounded subtraction underflow: {} - {} < 0", a, b),
))
} else { } else {
Ok(Value::Bounded(a - b)) Ok(Value::Bounded(a - b))
} }
@ -510,7 +592,10 @@ impl VirtualMachine {
(Value::Bounded(a), Value::Bounded(b)) => { (Value::Bounded(a), Value::Bounded(b)) => {
let res = a as u64 * b as u64; let res = a as u64 * b as u64;
if res > 0xFFFF { if res > 0xFFFF {
Err(OpError::Trap(TRAP_OOB, format!("Bounded multiplication overflow: {} * {} = {}", a, b, res))) Err(OpError::Trap(
TRAP_OOB,
format!("Bounded multiplication overflow: {} * {} = {}", a, b, res),
))
} else { } else {
Ok(Value::Bounded(res as u32)) Ok(Value::Bounded(res as u32))
} }
@ -520,25 +605,37 @@ impl VirtualMachine {
OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => { (Value::Int32(a), Value::Int32(b)) => {
if b == 0 { if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into())); return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
} }
Ok(Value::Int32(a / b)) Ok(Value::Int32(a / b))
} }
(Value::Int64(a), Value::Int64(b)) => { (Value::Int64(a), Value::Int64(b)) => {
if b == 0 { if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into())); return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
} }
Ok(Value::Int64(a / b)) Ok(Value::Int64(a / b))
} }
(Value::Int32(a), Value::Int64(b)) => { (Value::Int32(a), Value::Int64(b)) => {
if b == 0 { if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into())); return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
} }
Ok(Value::Int64(a as i64 / b)) Ok(Value::Int64(a as i64 / b))
} }
(Value::Int64(a), Value::Int32(b)) => { (Value::Int64(a), Value::Int32(b)) => {
if b == 0 { if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into())); return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
} }
Ok(Value::Int64(a / b as i64)) Ok(Value::Int64(a / b as i64))
} }
@ -574,7 +671,10 @@ impl VirtualMachine {
} }
(Value::Bounded(a), Value::Bounded(b)) => { (Value::Bounded(a), Value::Bounded(b)) => {
if b == 0 { if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Bounded division by zero".into())); return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Bounded division by zero".into(),
));
} }
Ok(Value::Bounded(a / b)) Ok(Value::Bounded(a / b))
} }
@ -606,19 +706,34 @@ impl VirtualMachine {
if let Value::Bounded(b) = val { if let Value::Bounded(b) = val {
self.push(Value::Int64(b as i64)); self.push(Value::Int64(b as i64));
} else { } else {
return Err(LogicalFrameEndingReason::Panic("Expected bounded for BOUND_TO_INT".into())); return Err(LogicalFrameEndingReason::Panic(
"Expected bounded for BOUND_TO_INT".into(),
));
} }
} }
OpCode::IntToBoundChecked => { OpCode::IntToBoundChecked => {
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let int_val = val.as_integer().ok_or_else(|| LogicalFrameEndingReason::Panic("Expected integer for INT_TO_BOUND_CHECKED".into()))?; let int_val = val.as_integer().ok_or_else(|| {
LogicalFrameEndingReason::Panic(
"Expected integer for INT_TO_BOUND_CHECKED".into(),
)
})?;
if int_val < 0 || int_val > 0xFFFF { if int_val < 0 || int_val > 0xFFFF {
return Err(self.trap(TRAP_OOB, OpCode::IntToBoundChecked as u16, format!("Integer to bounded conversion out of range: {}", int_val), start_pc as u32)); return Err(self.trap(
TRAP_OOB,
OpCode::IntToBoundChecked as u16,
format!("Integer to bounded conversion out of range: {}", int_val),
start_pc as u32,
));
} }
self.push(Value::Bounded(int_val as u32)); self.push(Value::Bounded(int_val as u32));
} }
OpCode::Eq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?, OpCode::Eq => {
OpCode::Neq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?, self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?
}
OpCode::Neq => {
self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?
}
OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| { OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| {
a.partial_cmp(&b) a.partial_cmp(&b)
.map(|o| Value::Boolean(o == std::cmp::Ordering::Less)) .map(|o| Value::Boolean(o == std::cmp::Ordering::Less))
@ -679,14 +794,18 @@ impl VirtualMachine {
OpCode::Shl => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { OpCode::Shl => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))), (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))), (Value::Int32(a), Value::Int64(b)) => {
Ok(Value::Int64((a as i64).wrapping_shl(b as u32)))
}
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
_ => Err(OpError::Panic("Invalid types for Shl".into())), _ => Err(OpError::Panic("Invalid types for Shl".into())),
})?, })?,
OpCode::Shr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) { OpCode::Shr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))), (Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), (Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))), (Value::Int32(a), Value::Int64(b)) => {
Ok(Value::Int64((a as i64).wrapping_shr(b as u32)))
}
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))), (Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
_ => Err(OpError::Panic("Invalid types for Shr".into())), _ => Err(OpError::Panic("Invalid types for Shr".into())),
})?, })?,
@ -696,16 +815,26 @@ impl VirtualMachine {
Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())), Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())),
Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())), Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())),
Value::Float(a) => self.push(Value::Float(-a)), Value::Float(a) => self.push(Value::Float(-a)),
_ => return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into())), _ => {
return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into()));
}
} }
} }
OpCode::GetGlobal => { OpCode::GetGlobal => {
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let idx = instr
let val = self.globals.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid global index".into()))?; .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.globals.get(idx).cloned().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Invalid global index".into())
})?;
self.push(val); self.push(val);
} }
OpCode::SetGlobal => { OpCode::SetGlobal => {
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let idx = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if idx >= self.globals.len() { if idx >= self.globals.len() {
self.globals.resize(idx + 1, Value::Null); self.globals.resize(idx + 1, Value::Null);
@ -713,39 +842,75 @@ impl VirtualMachine {
self.globals[idx] = val; self.globals[idx] = val;
} }
OpCode::GetLocal => { OpCode::GetLocal => {
let slot = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let slot = instr
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?; .imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let frame = self.call_stack.last().ok_or_else(|| {
LogicalFrameEndingReason::Panic("No active call frame".into())
})?;
let func = &self.program.functions[frame.func_idx]; let func = &self.program.functions[frame.func_idx];
crate::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32) crate::local_addressing::check_local_slot(
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?; func,
slot,
opcode as u16,
start_pc as u32,
)
.map_err(|trap_info| {
self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc)
})?;
let stack_idx = crate::local_addressing::local_index(frame, slot); let stack_idx = crate::local_addressing::local_index(frame, slot);
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?; let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| {
LogicalFrameEndingReason::Panic(
"Internal error: validated local slot not found in stack".into(),
)
})?;
self.push(val); self.push(val);
} }
OpCode::SetLocal => { OpCode::SetLocal => {
let slot = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let slot = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?; let frame = self.call_stack.last().ok_or_else(|| {
LogicalFrameEndingReason::Panic("No active call frame".into())
})?;
let func = &self.program.functions[frame.func_idx]; let func = &self.program.functions[frame.func_idx];
crate::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32) crate::local_addressing::check_local_slot(
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?; func,
slot,
opcode as u16,
start_pc as u32,
)
.map_err(|trap_info| {
self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc)
})?;
let stack_idx = crate::local_addressing::local_index(frame, slot); let stack_idx = crate::local_addressing::local_index(frame, slot);
self.operand_stack[stack_idx] = val; self.operand_stack[stack_idx] = val;
} }
OpCode::Call => { OpCode::Call => {
let func_id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let func_id = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let callee = self.program.functions.get(func_id).ok_or_else(|| { let callee = self.program.functions.get(func_id).ok_or_else(|| {
self.trap(TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {}", func_id), start_pc as u32) self.trap(
TRAP_INVALID_FUNC,
opcode as u16,
format!("Invalid func_id {}", func_id),
start_pc as u32,
)
})?; })?;
if self.operand_stack.len() < callee.param_slots as usize { if self.operand_stack.len() < callee.param_slots as usize {
return Err(LogicalFrameEndingReason::Panic(format!( return Err(LogicalFrameEndingReason::Panic(format!(
"Stack underflow during CALL to func {}: expected at least {} arguments, got {}", "Stack underflow during CALL to func {}: expected at least {} arguments, got {}",
func_id, callee.param_slots, self.operand_stack.len() func_id,
callee.param_slots,
self.operand_stack.len()
))); )));
} }
@ -764,12 +929,17 @@ impl VirtualMachine {
self.pc = callee.code_offset as usize; self.pc = callee.code_offset as usize;
} }
OpCode::Ret => { OpCode::Ret => {
let frame = self.call_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Call stack underflow".into()))?; let frame = self.call_stack.pop().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Call stack underflow".into())
})?;
let func = &self.program.functions[frame.func_idx]; let func = &self.program.functions[frame.func_idx];
let return_slots = func.return_slots as usize; let return_slots = func.return_slots as usize;
let current_height = self.operand_stack.len(); let current_height = self.operand_stack.len();
let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots; let expected_height = frame.stack_base
+ func.param_slots as usize
+ func.local_slots as usize
+ return_slots;
if current_height != expected_height { if current_height != expected_height {
return Err(self.trap(TRAP_BAD_RET_SLOTS, opcode as u16, format!( return Err(self.trap(TRAP_BAD_RET_SLOTS, opcode as u16, format!(
@ -792,17 +962,19 @@ impl VirtualMachine {
self.pc = frame.return_pc as usize; self.pc = frame.return_pc as usize;
} }
OpCode::PushScope => { OpCode::PushScope => {
self.scope_stack.push(ScopeFrame { self.scope_stack.push(ScopeFrame { scope_stack_base: self.operand_stack.len() });
scope_stack_base: self.operand_stack.len(),
});
} }
OpCode::PopScope => { OpCode::PopScope => {
let frame = self.scope_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Scope stack underflow".into()))?; let frame = self.scope_stack.pop().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Scope stack underflow".into())
})?;
self.operand_stack.truncate(frame.scope_stack_base); self.operand_stack.truncate(frame.scope_stack_base);
} }
OpCode::Alloc => { OpCode::Alloc => {
// Allocate a new gate with given type and number of slots. // Allocate a new gate with given type and number of slots.
let (type_id, slots_u32) = instr.imm_u32x2().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let (type_id, slots_u32) = instr
.imm_u32x2()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let slots = slots_u32 as usize; let slots = slots_u32 as usize;
// Bump-allocate on the heap and zero-initialize with Null. // Bump-allocate on the heap and zero-initialize with Null.
@ -812,12 +984,8 @@ impl VirtualMachine {
} }
// Insert entry into gate pool; GateId is index in this pool. // Insert entry into gate pool; GateId is index in this pool.
let entry = GateEntry { let entry =
alive: true, GateEntry { alive: true, base: base_idx as u32, slots: slots_u32, type_id };
base: base_idx as u32,
slots: slots_u32,
type_id,
};
let gate_id = self.gate_pool.len() as u32; let gate_id = self.gate_pool.len() as u32;
self.gate_pool.push(entry); self.gate_pool.push(entry);
@ -825,7 +993,10 @@ impl VirtualMachine {
self.push(Value::Gate(gate_id as usize)); self.push(Value::Gate(gate_id as usize));
} }
OpCode::GateLoad => { OpCode::GateLoad => {
let offset = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let offset = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Gate(gid_usize) = ref_val { if let Value::Gate(gid_usize) = ref_val {
let gid = gid_usize as GateId; let gid = gid_usize as GateId;
@ -835,20 +1006,38 @@ impl VirtualMachine {
// bounds check against slots // bounds check against slots
let off_u32 = offset as u32; let off_u32 = offset as u32;
if off_u32 >= entry.slots { if off_u32 >= entry.slots {
return Err(self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)); return Err(self.trap(
TRAP_OOB,
OpCode::GateLoad as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
));
} }
let heap_idx = entry.base as usize + offset; let heap_idx = entry.base as usize + offset;
let val = self.heap.get(heap_idx).cloned().ok_or_else(|| { let val = self.heap.get(heap_idx).cloned().ok_or_else(|| {
// Should not happen if pool and heap are consistent, but keep defensive. // Should not happen if pool and heap are consistent, but keep defensive.
self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32) self.trap(
TRAP_OOB,
OpCode::GateLoad as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
)
})?; })?;
self.push(val); self.push(val);
} else { } else {
return Err(self.trap(TRAP_TYPE, OpCode::GateLoad as u16, "Expected gate handle for GATE_LOAD".to_string(), start_pc as u32)); return Err(self.trap(
TRAP_TYPE,
OpCode::GateLoad as u16,
"Expected gate handle for GATE_LOAD".to_string(),
start_pc as u32,
));
} }
} }
OpCode::GateStore => { OpCode::GateStore => {
let offset = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize; let offset = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Gate(gid_usize) = ref_val { if let Value::Gate(gid_usize) = ref_val {
@ -858,30 +1047,54 @@ impl VirtualMachine {
.map_err(|t| t)?; .map_err(|t| t)?;
let off_u32 = offset as u32; let off_u32 = offset as u32;
if off_u32 >= entry.slots { if off_u32 >= entry.slots {
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)); return Err(self.trap(
TRAP_OOB,
OpCode::GateStore as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
));
} }
let heap_idx = entry.base as usize + offset; let heap_idx = entry.base as usize + offset;
if heap_idx >= self.heap.len() { if heap_idx >= self.heap.len() {
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)); return Err(self.trap(
TRAP_OOB,
OpCode::GateStore as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
));
} }
self.heap[heap_idx] = val; self.heap[heap_idx] = val;
} else { } else {
return Err(self.trap(TRAP_TYPE, OpCode::GateStore as u16, "Expected gate handle for GATE_STORE".to_string(), start_pc as u32)); return Err(self.trap(
TRAP_TYPE,
OpCode::GateStore as u16,
"Expected gate handle for GATE_STORE".to_string(),
start_pc as u32,
));
} }
} }
OpCode::GateBeginPeek | OpCode::GateEndPeek | OpCode::GateBeginPeek
OpCode::GateBeginBorrow | OpCode::GateEndBorrow | | OpCode::GateEndPeek
OpCode::GateBeginMutate | OpCode::GateEndMutate | | OpCode::GateBeginBorrow
OpCode::GateRetain => { | OpCode::GateEndBorrow
} | OpCode::GateBeginMutate
| OpCode::GateEndMutate
| OpCode::GateRetain => {}
OpCode::GateRelease => { OpCode::GateRelease => {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?; self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
} }
OpCode::Syscall => { OpCode::Syscall => {
let pc_at_syscall = start_pc as u32; let pc_at_syscall = start_pc as u32;
let id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?; let id = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let syscall = prometeu_hal::syscalls::Syscall::from_u32(id).ok_or_else(|| { let syscall = prometeu_hal::syscalls::Syscall::from_u32(id).ok_or_else(|| {
self.trap(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(); let args_count = syscall.args_count();
@ -889,7 +1102,12 @@ impl VirtualMachine {
let mut args = Vec::with_capacity(args_count); let mut args = Vec::with_capacity(args_count);
for _ in 0..args_count { for _ in 0..args_count {
let v = self.pop().map_err(|_e| { let v = self.pop().map_err(|_e| {
self.trap(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); args.push(v);
} }
@ -898,9 +1116,13 @@ impl VirtualMachine {
let stack_height_before = self.operand_stack.len(); let stack_height_before = self.operand_stack.len();
let mut ret = crate::HostReturn::new(&mut self.operand_stack); let mut ret = crate::HostReturn::new(&mut self.operand_stack);
native.syscall(id, &args, &mut ret, ctx).map_err(|fault| match fault { native.syscall(id, &args, &mut ret, ctx).map_err(|fault| match fault {
VmFault::Trap(code, msg) => self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall), VmFault::Trap(code, msg) => {
self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall)
}
VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg), VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg),
VmFault::Unavailable => LogicalFrameEndingReason::Panic("Host feature unavailable".into()), VmFault::Unavailable => {
LogicalFrameEndingReason::Panic("Host feature unavailable".into())
}
})?; })?;
let stack_height_after = self.operand_stack.len(); let stack_height_after = self.operand_stack.len();
@ -908,7 +1130,10 @@ impl VirtualMachine {
if results_pushed != syscall.results_count() { if results_pushed != syscall.results_count() {
return Err(LogicalFrameEndingReason::Panic(format!( return Err(LogicalFrameEndingReason::Panic(format!(
"Syscall {} (0x{:08X}) results mismatch: expected {}, got {}", "Syscall {} (0x{:08X}) results mismatch: expected {}, got {}",
syscall.name(), id, syscall.results_count(), results_pushed syscall.name(),
id,
syscall.results_count(),
results_pushed
))); )));
} }
} }
@ -923,15 +1148,15 @@ impl VirtualMachine {
} }
/// Resolves a `GateId` to an immutable reference to its `GateEntry` or returns a trap reason. /// Resolves a `GateId` to an immutable reference to its `GateEntry` or returns a trap reason.
fn resolve_gate(&self, gate_id: GateId, opcode: u16, pc: u32) -> Result<&GateEntry, LogicalFrameEndingReason> { fn resolve_gate(
&self,
gate_id: GateId,
opcode: u16,
pc: u32,
) -> Result<&GateEntry, LogicalFrameEndingReason> {
let idx = gate_id as usize; let idx = gate_id as usize;
let entry = self.gate_pool.get(idx).ok_or_else(|| { let entry = self.gate_pool.get(idx).ok_or_else(|| {
self.trap( self.trap(TRAP_INVALID_GATE, opcode, format!("Invalid gate id: {}", gate_id), pc)
TRAP_INVALID_GATE,
opcode,
format!("Invalid gate id: {}", gate_id),
pc,
)
})?; })?;
if !entry.alive { if !entry.alive {
return Err(self.trap( return Err(self.trap(
@ -944,7 +1169,13 @@ impl VirtualMachine {
Ok(entry) Ok(entry)
} }
pub fn trap(&self, code: u32, opcode: u16, message: String, pc: u32) -> LogicalFrameEndingReason { pub fn trap(
&self,
code: u32,
opcode: u16,
message: String,
pc: u32,
) -> LogicalFrameEndingReason {
LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc)) LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc))
} }
@ -973,7 +1204,12 @@ impl VirtualMachine {
self.operand_stack.last().ok_or("Stack underflow".into()) self.operand_stack.last().ok_or("Stack underflow".into())
} }
fn binary_op<F>(&mut self, opcode: OpCode, start_pc: u32, f: F) -> Result<(), LogicalFrameEndingReason> fn binary_op<F>(
&mut self,
opcode: OpCode,
start_pc: u32,
f: F,
) -> Result<(), LogicalFrameEndingReason>
where where
F: FnOnce(Value, Value) -> Result<Value, OpError>, F: FnOnce(Value, Value) -> Result<Value, OpError>,
{ {
@ -1010,7 +1246,13 @@ mod tests {
struct MockNative; struct MockNative;
impl NativeInterface for MockNative { impl NativeInterface for MockNative {
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { fn syscall(
&mut self,
_id: u32,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
Ok(()) Ok(())
} }
} }
@ -1254,7 +1496,13 @@ mod tests {
]; ];
let mut vm = VirtualMachine { let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()), program: ProgramImage::new(
rom,
vec![],
functions,
None,
std::collections::HashMap::new(),
),
..Default::default() ..Default::default()
}; };
vm.prepare_call("0"); vm.prepare_call("0");
@ -1299,7 +1547,13 @@ mod tests {
]; ];
let mut vm = VirtualMachine { let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()), program: ProgramImage::new(
rom,
vec![],
functions,
None,
std::collections::HashMap::new(),
),
..Default::default() ..Default::default()
}; };
vm.prepare_call("0"); vm.prepare_call("0");
@ -1338,7 +1592,13 @@ mod tests {
]; ];
let mut vm2 = VirtualMachine { let mut vm2 = VirtualMachine {
program: ProgramImage::new(rom2, vec![], functions2, None, std::collections::HashMap::new()), program: ProgramImage::new(
rom2,
vec![],
functions2,
None,
std::collections::HashMap::new(),
),
..Default::default() ..Default::default()
}; };
vm2.prepare_call("0"); vm2.prepare_call("0");
@ -1447,7 +1707,13 @@ mod tests {
]; ];
let mut vm = VirtualMachine { let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()), program: ProgramImage::new(
rom,
vec![],
functions,
None,
std::collections::HashMap::new(),
),
..Default::default() ..Default::default()
}; };
vm.prepare_call("0"); vm.prepare_call("0");
@ -1782,7 +2048,7 @@ mod tests {
0x17, 0x00, // PushI32 0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0 0x00, 0x00, 0x00, 0x00, // value 0
0x11, 0x00, // Pop 0x11, 0x00, // Pop
0x51, 0x00 // Ret 0x51, 0x00, // Ret
]; ];
let mut vm = VirtualMachine::new(rom.clone(), vec![]); let mut vm = VirtualMachine::new(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta { vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta {
@ -1793,7 +2059,15 @@ mod tests {
let mut ctx = HostContext::new(None); let mut ctx = HostContext::new(None);
struct TestNative; struct TestNative;
impl NativeInterface for TestNative { impl NativeInterface for TestNative {
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { Ok(()) } fn syscall(
&mut self,
_id: u32,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
Ok(())
}
} }
let mut native = TestNative; let mut native = TestNative;
@ -1811,7 +2085,13 @@ mod tests {
struct MultiReturnNative; struct MultiReturnNative;
impl NativeInterface for MultiReturnNative { impl NativeInterface for MultiReturnNative {
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { fn syscall(
&mut self,
_id: u32,
_args: &[Value],
ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
ret.push_bool(true); ret.push_bool(true);
ret.push_int(42); ret.push_int(42);
ret.push_bounded(255)?; ret.push_bounded(255)?;
@ -1845,7 +2125,13 @@ mod tests {
struct VoidReturnNative; struct VoidReturnNative;
impl NativeInterface for VoidReturnNative { impl NativeInterface for VoidReturnNative {
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { fn syscall(
&mut self,
_id: u32,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
Ok(()) Ok(())
} }
} }
@ -1879,7 +2165,13 @@ mod tests {
struct ArgCheckNative; struct ArgCheckNative;
impl NativeInterface for ArgCheckNative { impl NativeInterface for ArgCheckNative {
fn syscall(&mut self, _id: u32, args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { fn syscall(
&mut self,
_id: u32,
args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
expect_int(args, 0)?; expect_int(args, 0)?;
Ok(()) Ok(())
} }
@ -1967,7 +2259,13 @@ mod tests {
struct BadNative; struct BadNative;
impl NativeInterface for BadNative { impl NativeInterface for BadNative {
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { fn syscall(
&mut self,
_id: u32,
_args: &[Value],
ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
// Wrong: GfxClear565 is void but we push something // Wrong: GfxClear565 is void but we push something
ret.push_int(42); ret.push_int(42);
Ok(()) Ok(())
@ -2029,7 +2327,7 @@ mod tests {
let res = vm.initialize(header, ""); let res = vm.initialize(header, "");
match res { match res {
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {}, Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {}
_ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res), _ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res),
} }
} }
@ -2065,7 +2363,6 @@ mod tests {
assert_eq!(vm.cycles, 0); assert_eq!(vm.cycles, 0);
} }
#[test] #[test]
fn test_calling_convention_add() { fn test_calling_convention_add() {
let mut native = MockNative; let mut native = MockNative;

View File

@ -0,0 +1,29 @@
//! Minimal, robust smoke test for the VM crate.
//!
//! Intentionally does not assert legacy ISA behavior. It only ensures that:
//! - The VM type can be instantiated without panicking.
//! - Deterministic test utilities work as expected (reproducible RNG).
use prometeu_test_support::{next_u64, rng_from_seed};
use prometeu_vm::VirtualMachine;
#[test]
fn vm_instantiation_is_stable() {
// Create a VM with empty ROM and empty constant pool.
let vm = VirtualMachine::new(vec![], vec![]);
// Basic invariant checks that should remain stable across refactors.
assert_eq!(vm.pc, 0);
assert!(!vm.halted);
}
#[test]
fn deterministic_rng_sequences_match() {
// Demonstrate deterministic behavior without relying on specific values.
let mut a = rng_from_seed(12345);
let mut b = rng_from_seed(12345);
for _ in 0..8 {
// Same seed => same sequence
assert_eq!(next_u64(&mut a), next_u64(&mut b));
}
}

View File

@ -0,0 +1,8 @@
[package]
name = "prometeu-test-support"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
rand = { version = "0.8", features = ["std", "std_rng"] }

View File

@ -0,0 +1,108 @@
//! Test-only utilities for deterministic behavior in unit tests.
//!
//! This crate is intended to be used as a dev-dependency only. It provides:
//! - Seeded RNG helpers for reproducible randomness in tests.
//! - A tiny deterministic clock abstraction for tests that need to reason about time.
use rand::{RngCore, SeedableRng, rngs::StdRng};
/// Builds a `StdRng` from a u64 seed in a deterministic way.
///
/// This expands the u64 seed into a 32-byte array (little-endian repeated)
/// to initialize `StdRng`.
pub fn rng_from_seed(seed: u64) -> StdRng {
let le = seed.to_le_bytes();
let mut buf = [0u8; 32];
// Repeat the 8-byte seed 4 times to fill 32 bytes
for i in 0..4 {
buf[i * 8..(i + 1) * 8].copy_from_slice(&le);
}
StdRng::from_seed(buf)
}
/// Convenience helper that returns a RNG with a fixed well-known seed.
pub fn deterministic_rng() -> StdRng {
rng_from_seed(0xC0FFEE_5EED)
}
/// Returns the next u64 from the provided RNG.
pub fn next_u64(rng: &mut StdRng) -> u64 {
rng.next_u64()
}
/// Collects `n` u64 values from the RNG.
pub fn take_n_u64(rng: &mut StdRng, n: usize) -> Vec<u64> {
let mut out = Vec::with_capacity(n);
for _ in 0..n {
out.push(rng.next_u64());
}
out
}
/// Simple deterministic clock abstraction for tests.
pub trait Clock {
fn now_millis(&self) -> u64;
}
/// A clock that always returns a fixed instant.
pub struct FixedClock {
now: u64,
}
impl FixedClock {
pub fn new(now: u64) -> Self {
Self { now }
}
}
impl Clock for FixedClock {
fn now_millis(&self) -> u64 {
self.now
}
}
/// A clock that advances by a constant step each tick.
pub struct TickingClock {
now: u64,
step: u64,
}
impl TickingClock {
pub fn new(start: u64, step: u64) -> Self {
Self { now: start, step }
}
pub fn tick(&mut self) {
self.now = self.now.saturating_add(self.step);
}
}
impl Clock for TickingClock {
fn now_millis(&self) -> u64 {
self.now
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rng_reproducible_sequences() {
let mut a = rng_from_seed(42);
let mut b = rng_from_seed(42);
for _ in 0..10 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn ticking_clock_advances_deterministically() {
let mut c = TickingClock::new(1000, 16);
assert_eq!(c.now_millis(), 1000);
c.tick();
assert_eq!(c.now_millis(), 1016);
c.tick();
assert_eq!(c.now_millis(), 1032);
}
}

View File

@ -1,10 +1,10 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use prometeu_drivers::{AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; use prometeu_drivers::{AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
use ringbuf::traits::{Consumer, Producer, Split}; use prometeu_hal::LoopMode;
use ringbuf::HeapRb; use ringbuf::HeapRb;
use ringbuf::traits::{Consumer, Producer, Split};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use prometeu_hal::LoopMode;
pub struct HostAudio { pub struct HostAudio {
pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>, pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
@ -14,18 +14,12 @@ pub struct HostAudio {
impl HostAudio { impl HostAudio {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { producer: None, perf_consumer: None, _stream: None }
producer: None,
perf_consumer: None,
_stream: None,
}
} }
pub fn init(&mut self) { pub fn init(&mut self) {
let host = cpal::default_host(); let host = cpal::default_host();
let device = host let device = host.default_output_device().expect("no output device available");
.default_output_device()
.expect("no output device available");
let config = cpal::StreamConfig { let config = cpal::StreamConfig {
channels: 2, channels: 2,
@ -94,26 +88,17 @@ pub struct AudioMixer {
impl AudioMixer { impl AudioMixer {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { voices: Default::default(), last_processing_time: Duration::ZERO, paused: false }
voices: Default::default(),
last_processing_time: Duration::ZERO,
paused: false,
}
} }
pub fn process_command(&mut self, cmd: AudioCommand) { pub fn process_command(&mut self, cmd: AudioCommand) {
match cmd { match cmd {
AudioCommand::Play { AudioCommand::Play { sample, voice_id, volume, pan, pitch, priority, loop_mode } => {
sample,
voice_id,
volume,
pan,
pitch,
priority,
loop_mode,
} => {
if voice_id < MAX_CHANNELS { if voice_id < MAX_CHANNELS {
println!("[AudioMixer] Playing voice {}: vol={}, pitch={}, loop={:?}", voice_id, volume, pitch, loop_mode); println!(
"[AudioMixer] Playing voice {}: vol={}, pitch={}, loop={:?}",
voice_id, volume, pitch, loop_mode
);
self.voices[voice_id] = Channel { self.voices[voice_id] = Channel {
sample: Some(sample), sample: Some(sample),
active: true, active: true,
@ -212,7 +197,8 @@ impl AudioMixer {
voice.pos += step; voice.pos += step;
let end_pos = sample_data.loop_end.map(|e| e as f64).unwrap_or(sample_data.data.len() as f64); let end_pos =
sample_data.loop_end.map(|e| e as f64).unwrap_or(sample_data.data.len() as f64);
if voice.pos >= end_pos { if voice.pos >= end_pos {
if voice.loop_mode == LoopMode::On { if voice.loop_mode == LoopMode::On {

View File

@ -2,16 +2,17 @@ use prometeu_hal::telemetry::CertificationConfig;
pub fn load_cap_config(path: &str) -> Option<CertificationConfig> { pub fn load_cap_config(path: &str) -> Option<CertificationConfig> {
let content = std::fs::read_to_string(path).ok()?; let content = std::fs::read_to_string(path).ok()?;
let mut config = CertificationConfig { let mut config = CertificationConfig { enabled: true, ..Default::default() };
enabled: true,
..Default::default()
};
for line in content.lines() { for line in content.lines() {
let line = line.trim(); let line = line.trim();
if line.is_empty() || line.starts_with('#') { continue; } if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split('=').collect(); let parts: Vec<&str> = line.split('=').collect();
if parts.len() != 2 { continue; } if parts.len() != 2 {
continue;
}
let key = parts[0].trim(); let key = parts[0].trim();
let val = parts[1].trim(); let val = parts[1].trim();

View File

@ -1,9 +1,9 @@
use prometeu_hal::debugger_protocol::*; use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware}; use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::debugger_protocol::*;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream}; use std::net::{TcpListener, TcpStream};
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::cartridge_loader::CartridgeLoader;
/// Host-side implementation of the PROMETEU DevTools Protocol. /// Host-side implementation of the PROMETEU DevTools Protocol.
/// ///
@ -136,7 +136,9 @@ impl HostDebugger {
// Support multiple JSON messages in a single TCP packet. // Support multiple JSON messages in a single TCP packet.
for line in msg.lines() { for line in msg.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.is_empty() { continue; } if trimmed.is_empty() {
continue;
}
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) { if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
self.handle_command(cmd, firmware, hardware); self.handle_command(cmd, firmware, hardware);
} }
@ -162,7 +164,12 @@ impl HostDebugger {
} }
/// Dispatches a specific DebugCommand to the system components. /// Dispatches a specific DebugCommand to the system components.
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) { fn handle_command(
&mut self,
cmd: DebugCommand,
firmware: &mut Firmware,
hardware: &mut Hardware,
) {
match cmd { match cmd {
DebugCommand::Ok | DebugCommand::Start => { DebugCommand::Ok | DebugCommand::Start => {
if self.waiting_for_start { if self.waiting_for_start {
@ -189,8 +196,7 @@ impl HostDebugger {
} }
DebugCommand::GetState => { DebugCommand::GetState => {
// Return detailed VM register and stack state. // Return detailed VM register and stack state.
let stack_top = firmware.vm.operand_stack.iter() let stack_top = firmware.vm.operand_stack.iter().rev().take(10).cloned().collect();
.rev().take(10).cloned().collect();
let resp = DebugResponse::GetState { let resp = DebugResponse::GetState {
pc: firmware.vm.pc, pc: firmware.vm.pc,

View File

@ -1,6 +1,6 @@
use prometeu_system::fs::{FsBackend, FsEntry, FsError};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use prometeu_system::fs::{FsBackend, FsEntry, FsError};
pub struct HostDirBackend { pub struct HostDirBackend {
root: PathBuf, root: PathBuf,
@ -90,7 +90,11 @@ mod tests {
fn get_temp_dir(name: &str) -> PathBuf { fn get_temp_dir(name: &str) -> PathBuf {
let mut path = env::temp_dir(); let mut path = env::temp_dir();
path.push(format!("prometeu_host_test_{}_{}", name, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos())); path.push(format!(
"prometeu_host_test_{}_{}",
name,
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
fs::create_dir_all(&path).unwrap(); fs::create_dir_all(&path).unwrap();
path path
} }

View File

@ -1,8 +1,8 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::InputSignals;
use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey}; use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::Window; use winit::window::Window;
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::InputSignals;
pub struct HostInputHandler { pub struct HostInputHandler {
pub signals: InputSignals, pub signals: InputSignals,
@ -10,9 +10,7 @@ pub struct HostInputHandler {
impl HostInputHandler { impl HostInputHandler {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { signals: InputSignals::default() }
signals: InputSignals::default(),
}
} }
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) { pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
@ -35,7 +33,9 @@ impl HostInputHandler {
KeyCode::KeyE => self.signals.r_signal = is_down, KeyCode::KeyE => self.signals.r_signal = is_down,
KeyCode::KeyZ => self.signals.start_signal = is_down, KeyCode::KeyZ => self.signals.start_signal = is_down,
KeyCode::ShiftLeft | KeyCode::ShiftRight => self.signals.select_signal = is_down, KeyCode::ShiftLeft | KeyCode::ShiftRight => {
self.signals.select_signal = is_down
}
_ => {} _ => {}
} }

View File

@ -1,11 +1,11 @@
pub mod audio; pub mod audio;
pub mod runner;
pub mod fs_backend;
pub mod log_sink;
pub mod debugger;
pub mod stats;
pub mod input;
pub mod cap; pub mod cap;
pub mod debugger;
pub mod fs_backend;
pub mod input;
pub mod log_sink;
pub mod runner;
pub mod stats;
pub mod utilities; pub mod utilities;
use cap::load_cap_config; use cap::load_cap_config;
@ -45,17 +45,9 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let cap_config = cli.cap.as_ref().and_then(|path| load_cap_config(path)); let cap_config = cli.cap.as_ref().and_then(|path| load_cap_config(path));
let boot_target = if let Some(path) = cli.debug { let boot_target = if let Some(path) = cli.debug {
BootTarget::Cartridge { BootTarget::Cartridge { path, debug: true, debug_port: cli.port }
path,
debug: true,
debug_port: cli.port,
}
} else if let Some(path) = cli.run { } else if let Some(path) = cli.run {
BootTarget::Cartridge { BootTarget::Cartridge { path, debug: false, debug_port: 7777 }
path,
debug: false,
debug_port: 7777,
}
} else { } else {
BootTarget::Hub BootTarget::Hub
}; };

View File

@ -7,7 +7,11 @@ use crate::stats::HostStats;
use crate::utilities::draw_rgb565_to_rgba8; use crate::utilities::draw_rgb565_to_rgba8;
use pixels::wgpu::PresentMode; use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture}; use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::AudioCommand;
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware}; use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::color::Color;
use prometeu_hal::telemetry::CertificationConfig;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
@ -15,10 +19,6 @@ use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey}; use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId}; use winit::window::{Window, WindowAttributes, WindowId};
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::AudioCommand;
use prometeu_hal::color::Color;
/// The Desktop implementation of the PROMETEU Runtime. /// The Desktop implementation of the PROMETEU Runtime.
/// ///
@ -130,8 +130,18 @@ impl HostRunner {
let color_warn = Color::RED; let color_warn = Color::RED;
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg); self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg);
self.hardware.gfx.draw_text(10, 10, &format!("FPS: {:.1}", self.stats.current_fps), color_text); self.hardware.gfx.draw_text(
self.hardware.gfx.draw_text(10, 18, &format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0), color_text); 10,
10,
&format!("FPS: {:.1}", self.stats.current_fps),
color_text,
);
self.hardware.gfx.draw_text(
10,
18,
&format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0),
color_text,
);
self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text); self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text);
self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text); self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text);
@ -140,25 +150,60 @@ impl HostRunner {
} else { } else {
0.0 0.0
}; };
self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct), color_text); self.hardware.gfx.draw_text(
10,
42,
&format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct),
color_text,
);
self.hardware.gfx.draw_text(10, 50, &format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied), color_text); self.hardware.gfx.draw_text(
10,
50,
&format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied),
color_text,
);
if tel.gfx_inflight_bytes > 0 { if tel.gfx_inflight_bytes > 0 {
self.hardware.gfx.draw_text(10, 58, &format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024), color_warn); self.hardware.gfx.draw_text(
10,
58,
&format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024),
color_warn,
);
} }
self.hardware.gfx.draw_text(10, 66, &format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied), color_text); self.hardware.gfx.draw_text(
10,
66,
&format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied),
color_text,
);
if tel.audio_inflight_bytes > 0 { if tel.audio_inflight_bytes > 0 {
self.hardware.gfx.draw_text(10, 74, &format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024), color_warn); self.hardware.gfx.draw_text(
10,
74,
&format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024),
color_warn,
);
} }
let cert_color = if tel.violations > 0 { color_warn } else { color_text }; let cert_color = if tel.violations > 0 { color_warn } else { color_text };
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color); self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color);
if tel.violations > 0 { if tel.violations > 0 {
if let Some(event) = self.firmware.os.log_service.get_recent(10).into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03) { if let Some(event) = self
.firmware
.os
.log_service
.get_recent(10)
.into_iter()
.rev()
.find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03)
{
let mut msg = event.msg.clone(); let mut msg = event.msg.clone();
if msg.len() > 30 { msg.truncate(30); } if msg.len() > 30 {
msg.truncate(30);
}
self.hardware.gfx.draw_text(10, 90, &msg, color_warn); self.hardware.gfx.draw_text(10, 90, &msg, color_warn);
} }
} }
@ -183,7 +228,8 @@ impl ApplicationHandler for HostRunner {
let size = window.inner_size(); let size = window.inner_size();
let surface_texture = SurfaceTexture::new(size.width, size.height, window); let surface_texture = SurfaceTexture::new(size.width, size.height, window);
let mut pixels = PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture) let mut pixels =
PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
.present_mode(PresentMode::Fifo) // activate vsync .present_mode(PresentMode::Fifo) // activate vsync
.build() .build()
.expect("failed to create Pixels"); .expect("failed to create Pixels");
@ -197,7 +243,6 @@ impl ApplicationHandler for HostRunner {
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
} }
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
self.input.handle_event(&event, self.window()); self.input.handle_event(&event, self.window());
@ -232,7 +277,6 @@ impl ApplicationHandler for HostRunner {
} }
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key { if let PhysicalKey::Code(code) = event.physical_key {
let is_down = event.state == ElementState::Pressed; let is_down = event.state == ElementState::Pressed;
@ -298,11 +342,8 @@ impl ApplicationHandler for HostRunner {
let is_paused = self.firmware.os.paused || self.debugger.waiting_for_start; let is_paused = self.firmware.os.paused || self.debugger.waiting_for_start;
if is_paused != self.last_paused_state { if is_paused != self.last_paused_state {
self.last_paused_state = is_paused; self.last_paused_state = is_paused;
let cmd = if is_paused { let cmd =
AudioCommand::MasterPause if is_paused { AudioCommand::MasterPause } else { AudioCommand::MasterResume };
} else {
AudioCommand::MasterResume
};
self.hardware.audio.commands.push(cmd); self.hardware.audio.commands.push(cmd);
} }
@ -345,9 +386,9 @@ impl ApplicationHandler for HostRunner {
mod tests { mod tests {
use super::*; use super::*;
use prometeu_firmware::BootTarget; use prometeu_firmware::BootTarget;
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
#[test] #[test]
fn test_debug_port_opens() { fn test_debug_port_opens() {
@ -364,7 +405,8 @@ mod tests {
// Check if we can connect // Check if we can connect
{ {
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect"); let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
// Short sleep to ensure the OS processes // Short sleep to ensure the OS processes
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(100));
@ -375,24 +417,36 @@ mod tests {
// Handshake Check // Handshake Check
let mut buf = [0u8; 2048]; let mut buf = [0u8; 2048];
let n = stream.read(&mut buf).expect("Should read handshake"); let n = stream.read(&mut buf).expect("Should read handshake");
let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON"); let resp: serde_json::Value =
serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
assert_eq!(resp["type"], "handshake"); assert_eq!(resp["type"], "handshake");
assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION); assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION);
// Send start via JSON // Send start via JSON
stream.write_all(b"{\"type\":\"start\"}\n").expect("Connection should be open for writing"); stream
.write_all(b"{\"type\":\"start\"}\n")
.expect("Connection should be open for writing");
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
// Process the received command // Process the received command
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(!runner.debugger.waiting_for_start, "Execution should have started after start command"); assert!(
assert!(runner.debugger.listener.is_some(), "Listener should remain open for reconnections"); !runner.debugger.waiting_for_start,
"Execution should have started after start command"
);
assert!(
runner.debugger.listener.is_some(),
"Listener should remain open for reconnections"
);
} }
// Now that the stream is out of the test scope, the runner should detect closure on next check // Now that the stream is out of the test scope, the runner should detect closure on next check
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_none(), "Stream should have been closed after client disconnected"); assert!(
runner.debugger.stream.is_none(),
"Stream should have been closed after client disconnected"
);
} }
#[test] #[test]
@ -407,7 +461,8 @@ mod tests {
// 1. Connect and start // 1. Connect and start
{ {
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1"); let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some()); assert!(runner.debugger.stream.is_some());
@ -430,7 +485,10 @@ mod tests {
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some(), "Stream should have been accepted on reconnection"); assert!(
runner.debugger.stream.is_some(),
"Stream should have been accepted on reconnection"
);
} }
#[test] #[test]
@ -444,13 +502,15 @@ mod tests {
}); });
// 1. First connection // 1. First connection
let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1"); let mut _stream1 =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some()); assert!(runner.debugger.stream.is_some());
// 2. Second connection // 2. Second connection
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 2 (OS accepts)"); let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port))
.expect("Should connect 2 (OS accepts)");
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Should accept and close stream2 runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Should accept and close stream2
@ -458,7 +518,10 @@ mod tests {
let mut buf = [0u8; 10]; let mut buf = [0u8; 10];
stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap(); stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap();
let res = stream2.read(&mut buf); let res = stream2.read(&mut buf);
assert!(matches!(res, Ok(0)) || res.is_err(), "Second connection should be closed by server"); assert!(
matches!(res, Ok(0)) || res.is_err(),
"Second connection should be closed by server"
);
assert!(runner.debugger.stream.is_some(), "First connection should continue active"); assert!(runner.debugger.stream.is_some(), "First connection should continue active");
} }
@ -494,7 +557,9 @@ mod tests {
loop { loop {
line.clear(); line.clear();
reader.read_line(&mut line).expect("Should read line"); reader.read_line(&mut line).expect("Should read line");
if line.is_empty() { break; } if line.is_empty() {
break;
}
if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) { if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) {
if resp["type"] == "getState" { if resp["type"] == "getState" {
@ -517,7 +582,8 @@ mod tests {
// 1. Connect and pause // 1. Connect and pause
{ {
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect"); let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);

View File

@ -1,7 +1,7 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::Firmware; use prometeu_firmware::Firmware;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use winit::window::Window; use winit::window::Window;
use prometeu_drivers::hardware::Hardware;
pub struct HostStats { pub struct HostStats {
pub last_stats_update: Instant, pub last_stats_update: Instant,
@ -31,7 +31,13 @@ impl HostStats {
self.audio_load_samples += 1; self.audio_load_samples += 1;
} }
pub fn update(&mut self, now: Instant, window: Option<&Window>, _hardware: &Hardware, firmware: &Firmware) { pub fn update(
&mut self,
now: Instant,
window: Option<&Window>,
_hardware: &Hardware,
firmware: &Firmware,
) {
let stats_elapsed = now.duration_since(self.last_stats_update); let stats_elapsed = now.duration_since(self.last_stats_update);
if stats_elapsed >= Duration::from_secs(1) { if stats_elapsed >= Duration::from_secs(1) {
self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64(); self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
@ -39,7 +45,8 @@ impl HostStats {
if let Some(window) = window { if let Some(window) = window {
// Fixed comparison always against 60Hz, keep even when doing CPU stress tests // Fixed comparison always against 60Hz, keep even when doing CPU stress tests
let frame_budget_us = 16666.0; let frame_budget_us = 16666.0;
let cpu_load_core = (firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0; let cpu_load_core =
(firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
let cpu_load_audio = if self.audio_load_samples > 0 { let cpu_load_audio = if self.audio_load_samples > 0 {
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0 (self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
@ -49,7 +56,12 @@ impl HostStats {
let title = format!( let title = format!(
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}", "PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
0, self.current_fps, cpu_load_core, cpu_load_audio, firmware.os.tick_index, firmware.os.logical_frame_index 0,
self.current_fps,
cpu_load_core,
cpu_load_audio,
firmware.os.tick_index,
firmware.os.logical_frame_index
); );
window.set_title(&title); window.set_title(&title);
} }

View File

@ -1 +1,3 @@
fn main() -> Result<(), Box<dyn std::error::Error>> { prometeu_host_desktop_winit::run() } fn main() -> Result<(), Box<dyn std::error::Error>> {
prometeu_host_desktop_winit::run()
}

View File

@ -141,7 +141,8 @@ fn dispatch(exe_dir: &Path, bin_name: &str, args: &[&str]) {
"prometeuc" => "build/verify c", "prometeuc" => "build/verify c",
"prometeup" => "pack/verify p", "prometeup" => "pack/verify p",
_ => bin_name, _ => bin_name,
}, exe_dir.display() },
exe_dir.display()
); );
std::process::exit(1); std::process::exit(1);
} }
@ -168,9 +169,6 @@ fn execute_bin(bin_path: &Path, args: &[&str]) {
} }
fn not_implemented(cmd: &str, _bin_name: &str) { fn not_implemented(cmd: &str, _bin_name: &str) {
eprintln!( eprintln!("prometeu: command '{}' is not yet available in this distribution", cmd);
"prometeu: command '{}' is not yet available in this distribution",
cmd
);
std::process::exit(1); std::process::exit(1);
} }

25
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,25 @@
# Prometeu Runtime — Architecture (Reset Invariants)
This document captures the high-level invariants for the reset cycle. It is a stub and will
evolve as we formalize the new ISA/VM specs. The goal is to define non-negotiable properties
that guide refactors without forcing legacy compatibility.
Core invariants
---------------
- Stack-based VM with heap-allocated objects.
- Garbage Collection happens at safepoints, primarily at `FRAME_SYNC`.
- Closures are first-class (user functions). Syscalls are callable but not first-class values.
- Coroutines are the only concurrency model.
- No backward compatibility: old bytecode formats, shims, or legacy bridges are out of scope.
Scope of this stage
-------------------
- Establish tooling baselines (fmt, clippy, CI) and minimal smoke tests.
- Avoid encoding legacy ISA semantics in tests; keep tests focused on build confidence.
- Production code must remain free from test-only hooks; use dev-only utilities for determinism.
Out of scope (for now)
----------------------
- Detailed ISA definition and instruction semantics beyond what is needed to compile and run
smoke-level validations.
- Performance tuning or GC algorithm selection.

View File

@ -1,15 +0,0 @@
# PROMETEU Documentation
This directory contains the technical documentation and specifications for the PROMETEU project.
## Content
### 📜 [Specifications (Specs)](./specs)
Detailed documentation on system architecture, cartridge format, VM instruction set, and more.
- [Topic Index](specs/hardware/topics/table-of-contents.md)
### 🐞 [Debugger](./debugger)
Documentation on debugging tools and how to integrate new tools into the ecosystem.
### 🔌 [DevTools Protocol](../devtools)
Definition of the communication protocol used for real-time debugging and inspection.

49
docs/STYLE.md Normal file
View File

@ -0,0 +1,49 @@
# Prometeu Runtime — Style Guide
This document defines the baseline code style and commenting policy for the reset cycle.
Goals:
- Professional, consistent, and readable code.
- Deterministic and actionable error messages.
- English-only comments and docs.
General principles
------------------
- Rust edition: use the crates configured edition (currently 2024 across new crates).
- Keep modules small and cohesive; prefer explicit interfaces over glob imports.
- Avoid premature abstraction; refactor when duplication becomes meaningful.
Comments and documentation
--------------------------
- Language: English only.
- Use `///` for public API documentation comments and module-level docs (`//!`).
- Use `//` for local/internal notes that do not need to surface in docs.
- Write comments that explain “why,” not “what” (the code already shows “what”).
- Keep comments up to date with behavior; outdated comments should be removed or fixed.
Error messages
--------------
- Be clear, actionable, and deterministic.
- Include the failing condition and the expected invariant when useful.
- Avoid leaking internal jargon; prefer user-understandable phrasing.
- Do not rely on timing or nondeterminism for error reproduction.
Naming conventions
------------------
- Crates and modules: `kebab-case` for crates, `snake_case` for modules and files.
- Types and traits: `PascalCase` (e.g., `VirtualMachine`, `BytecodeLoader`).
- Functions, methods, and variables: `snake_case`.
- Constants and statics: `SCREAMING_SNAKE_CASE`.
- Avoid abbreviations unless they are widely recognized (e.g., `pc`, `vm`).
Documentation structure
-----------------------
- Each public crate should have a crate-level docs section describing its purpose.
- Prefer small examples in docs that compile (use `rustdoc` code blocks when feasible).
- Keep module/file headers brief and focused.
Formatting and linting
----------------------
- `cargo fmt` is the source of truth for formatting.
- `cargo clippy` should run clean on the default toolchain. Where a lint is intentionally
violated, add a focused `#[allow(lint_name)]` with a short rationale.

View File

@ -1,19 +0,0 @@
### Phase 03 — Frontend API Boundary (Canon Contract)
This document codifies the FE/BE boundary invariants for Phase 03.
- BE is the source of truth. The `frontend-api` crate defines canonical models that all Frontends must produce.
- No string protocols across layers. Strings are only for display/debug. No hidden prefixes like `svc:` or `@dep:`.
- No FE implementation imports from other FE implementations.
- No BE imports PBS modules (hard boundary). The Backend consumes only canonical data structures from `frontend-api`.
- Overload resolution is signature-based. Arity alone is not sufficient; use canonical signatures/keys.
Implementation notes (PBS):
- The PBS adapter must not synthesize ownership or module info from string prefixes. All owner/module data should come from canonical types.
- Export/import surfaces are expressed exclusively via `frontend-api` types (e.g., `ItemName`, `ProjectAlias`, `ModulePath`, `ImportRef`, `ExportItem`).
Enforcement:
- A test in `prometeu-build-pipeline` scans `src/backend/**` to ensure no references to `frontends::pbs` are introduced.
- Code review should reject any PRs that reintroduce prefix-based heuristics or FE-to-FE coupling.

3
rustfmt.toml Normal file
View File

@ -0,0 +1,3 @@
max_width = 100
use_small_heuristics = "Max"
newline_style = "Unix"