pr8.1
This commit is contained in:
parent
25d4f3eacb
commit
bd507aeaa5
190
crates/console/prometeu-bytecode/src/assembler.rs
Normal file
190
crates/console/prometeu-bytecode/src/assembler.rs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
//! Minimal deterministic assembler for the canonical disassembly format.
|
||||||
|
//!
|
||||||
|
//! This is intended primarily for roundtrip tests: `bytes -> disassemble -> assemble -> bytes`.
|
||||||
|
//! It supports all mnemonics emitted by `disassembler.rs` and their operand formats.
|
||||||
|
|
||||||
|
use crate::isa::core::CoreOpCode;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum AsmError {
|
||||||
|
EmptyLine,
|
||||||
|
UnknownMnemonic(String),
|
||||||
|
UnexpectedOperand(String),
|
||||||
|
MissingOperand(String),
|
||||||
|
InvalidOperand(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_u16(v: u16, out: &mut Vec<u8>) { out.extend_from_slice(&v.to_le_bytes()); }
|
||||||
|
fn emit_u32(v: u32, out: &mut Vec<u8>) { out.extend_from_slice(&v.to_le_bytes()); }
|
||||||
|
fn emit_i32(v: i32, out: &mut Vec<u8>) { out.extend_from_slice(&v.to_le_bytes()); }
|
||||||
|
fn emit_i64(v: i64, out: &mut Vec<u8>) { out.extend_from_slice(&v.to_le_bytes()); }
|
||||||
|
fn emit_f64_bits(bits: u64, out: &mut Vec<u8>) { out.extend_from_slice(&bits.to_le_bytes()); }
|
||||||
|
|
||||||
|
fn parse_u32_any(s: &str) -> Result<u32, AsmError> {
|
||||||
|
let s = s.trim();
|
||||||
|
if let Some(rest) = s.strip_prefix("0x") { u32::from_str_radix(rest, 16).map_err(|_| AsmError::InvalidOperand(s.into())) } else { s.parse::<u32>().map_err(|_| AsmError::InvalidOperand(s.into())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_i32_any(s: &str) -> Result<i32, AsmError> {
|
||||||
|
s.trim().parse::<i32>().map_err(|_| AsmError::InvalidOperand(s.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_i64_any(s: &str) -> Result<i64, AsmError> {
|
||||||
|
s.trim().parse::<i64>().map_err(|_| AsmError::InvalidOperand(s.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_f64_bits(s: &str) -> Result<u64, AsmError> {
|
||||||
|
let s = s.trim();
|
||||||
|
let s = s.strip_prefix("f64:").ok_or_else(|| AsmError::InvalidOperand(s.into()))?;
|
||||||
|
let hex = s.strip_prefix("0x").ok_or_else(|| AsmError::InvalidOperand(s.into()))?;
|
||||||
|
if hex.len() != 16 { return Err(AsmError::InvalidOperand(s.into())); }
|
||||||
|
u64::from_str_radix(hex, 16).map_err(|_| AsmError::InvalidOperand(s.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_keyvals(s: &str) -> Result<(&str, &str), AsmError> {
|
||||||
|
// Parses formats like: "fn=123, captures=2" or "fn=3, argc=1"
|
||||||
|
let mut parts = s.split(',');
|
||||||
|
let a = parts.next().ok_or_else(|| AsmError::MissingOperand(s.into()))?.trim();
|
||||||
|
let b = parts.next().ok_or_else(|| AsmError::MissingOperand(s.into()))?.trim();
|
||||||
|
if parts.next().is_some() { return Err(AsmError::InvalidOperand(s.into())); }
|
||||||
|
Ok((a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pair<'a>(a: &'a str, ka: &str, b: &'a str, kb: &str) -> Result<(u32,u32), AsmError> {
|
||||||
|
let (ka_l, va_s) = a.split_once('=').ok_or_else(|| AsmError::InvalidOperand(a.into()))?;
|
||||||
|
let (kb_l, vb_s) = b.split_once('=').ok_or_else(|| AsmError::InvalidOperand(b.into()))?;
|
||||||
|
if ka_l.trim() != ka || kb_l.trim() != kb { return Err(AsmError::InvalidOperand(format!("expected keys {} and {}", ka, kb))); }
|
||||||
|
let va = parse_u32_any(va_s)?;
|
||||||
|
let vb = parse_u32_any(vb_s)?;
|
||||||
|
Ok((va, vb))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mnemonic(line: &str) -> (&str, &str) {
|
||||||
|
let line = line.trim();
|
||||||
|
if let Some(sp) = line.find(char::is_whitespace) {
|
||||||
|
let (mn, rest) = line.split_at(sp);
|
||||||
|
(mn, rest.trim())
|
||||||
|
} else {
|
||||||
|
(line, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assemble(src: &str) -> Result<Vec<u8>, AsmError> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for raw_line in src.lines() {
|
||||||
|
let line = raw_line.trim();
|
||||||
|
if line.is_empty() { continue; }
|
||||||
|
let (mn, ops) = parse_mnemonic(line);
|
||||||
|
match mn {
|
||||||
|
// Zero-operand
|
||||||
|
"NOP" => { emit_u16(CoreOpCode::Nop as u16, &mut out); }
|
||||||
|
"HALT" => { emit_u16(CoreOpCode::Halt as u16, &mut out); }
|
||||||
|
"TRAP" => { emit_u16(CoreOpCode::Trap as u16, &mut out); }
|
||||||
|
"DUP" => { emit_u16(CoreOpCode::Dup as u16, &mut out); }
|
||||||
|
"SWAP" => { emit_u16(CoreOpCode::Swap as u16, &mut out); }
|
||||||
|
"ADD" => { emit_u16(CoreOpCode::Add as u16, &mut out); }
|
||||||
|
"SUB" => { emit_u16(CoreOpCode::Sub as u16, &mut out); }
|
||||||
|
"MUL" => { emit_u16(CoreOpCode::Mul as u16, &mut out); }
|
||||||
|
"DIV" => { emit_u16(CoreOpCode::Div as u16, &mut out); }
|
||||||
|
"MOD" => { emit_u16(CoreOpCode::Mod as u16, &mut out); }
|
||||||
|
"NEG" => { emit_u16(CoreOpCode::Neg as u16, &mut out); }
|
||||||
|
"EQ" => { emit_u16(CoreOpCode::Eq as u16, &mut out); }
|
||||||
|
"NEQ" => { emit_u16(CoreOpCode::Neq as u16, &mut out); }
|
||||||
|
"LT" => { emit_u16(CoreOpCode::Lt as u16, &mut out); }
|
||||||
|
"LTE" => { emit_u16(CoreOpCode::Lte as u16, &mut out); }
|
||||||
|
"GT" => { emit_u16(CoreOpCode::Gt as u16, &mut out); }
|
||||||
|
"GTE" => { emit_u16(CoreOpCode::Gte as u16, &mut out); }
|
||||||
|
"AND" => { emit_u16(CoreOpCode::And as u16, &mut out); }
|
||||||
|
"OR" => { emit_u16(CoreOpCode::Or as u16, &mut out); }
|
||||||
|
"NOT" => { emit_u16(CoreOpCode::Not as u16, &mut out); }
|
||||||
|
"BIT_AND" => { emit_u16(CoreOpCode::BitAnd as u16, &mut out); }
|
||||||
|
"BIT_OR" => { emit_u16(CoreOpCode::BitOr as u16, &mut out); }
|
||||||
|
"BIT_XOR" => { emit_u16(CoreOpCode::BitXor as u16, &mut out); }
|
||||||
|
"SHL" => { emit_u16(CoreOpCode::Shl as u16, &mut out); }
|
||||||
|
"SHR" => { emit_u16(CoreOpCode::Shr as u16, &mut out); }
|
||||||
|
"RET" => { emit_u16(CoreOpCode::Ret as u16, &mut out); }
|
||||||
|
"YIELD" => { emit_u16(CoreOpCode::Yield as u16, &mut out); }
|
||||||
|
"FRAME_SYNC" => { emit_u16(CoreOpCode::FrameSync as u16, &mut out); }
|
||||||
|
|
||||||
|
// One u32 immediate (decimal or hex accepted for SYSCALL only; others decimal ok)
|
||||||
|
"JMP" => {
|
||||||
|
if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::Jmp as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"JMP_IF_FALSE" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::JmpIfFalse as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"JMP_IF_TRUE" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::JmpIfTrue as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"PUSH_CONST" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::PushConst as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"PUSH_I64" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::PushI64 as u16, &mut out); emit_i64(parse_i64_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"PUSH_F64" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::PushF64 as u16, &mut out); emit_f64_bits(parse_f64_bits(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"PUSH_BOOL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
let v = parse_u32_any(ops)? as u8; emit_u16(CoreOpCode::PushBool as u16, &mut out); out.push(v);
|
||||||
|
}
|
||||||
|
"PUSH_I32" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::PushI32 as u16, &mut out); emit_i32(parse_i32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"POP_N" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::PopN as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"PUSH_BOUNDED" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::PushBounded as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"GET_GLOBAL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::GetGlobal as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"SET_GLOBAL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::SetGlobal as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"GET_LOCAL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::GetLocal as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"SET_LOCAL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::SetLocal as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"CALL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::Call as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"CALL_CLOSURE" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
let (k, v) = ops.split_once('=').ok_or_else(|| AsmError::InvalidOperand(ops.into()))?; if k.trim() != "argc" { return Err(AsmError::InvalidOperand(ops.into())); }
|
||||||
|
emit_u16(CoreOpCode::CallClosure as u16, &mut out); emit_u32(parse_u32_any(v)?, &mut out);
|
||||||
|
}
|
||||||
|
"MAKE_CLOSURE" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
let (a,b) = parse_keyvals(ops)?;
|
||||||
|
// Accept either order but require exact key names
|
||||||
|
let (fn_id, captures) = if a.starts_with("fn=") && b.starts_with("captures=") {
|
||||||
|
parse_pair(a, "fn", b, "captures")?
|
||||||
|
} else if a.starts_with("captures=") && b.starts_with("fn=") {
|
||||||
|
let (cap, fid) = parse_pair(a, "captures", b, "fn")?; (fid, cap)
|
||||||
|
} else { return Err(AsmError::InvalidOperand(ops.into())); };
|
||||||
|
emit_u16(CoreOpCode::MakeClosure as u16, &mut out); emit_u32(fn_id, &mut out); emit_u32(captures, &mut out);
|
||||||
|
}
|
||||||
|
"SPAWN" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
let (a,b) = parse_keyvals(ops)?;
|
||||||
|
let (fn_id, argc) = if a.starts_with("fn=") && b.starts_with("argc=") {
|
||||||
|
parse_pair(a, "fn", b, "argc")?
|
||||||
|
} else if a.starts_with("argc=") && b.starts_with("fn=") {
|
||||||
|
let (ac, fid) = parse_pair(a, "argc", b, "fn")?; (fid, ac)
|
||||||
|
} else { return Err(AsmError::InvalidOperand(ops.into())); };
|
||||||
|
emit_u16(CoreOpCode::Spawn as u16, &mut out); emit_u32(fn_id, &mut out); emit_u32(argc, &mut out);
|
||||||
|
}
|
||||||
|
"SLEEP" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::Sleep as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
"SYSCALL" => { if ops.is_empty() { return Err(AsmError::MissingOperand(line.into())); }
|
||||||
|
emit_u16(CoreOpCode::Syscall as u16, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
|
||||||
|
other => return Err(AsmError::UnknownMnemonic(other.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
115
crates/console/prometeu-bytecode/src/disassembler.rs
Normal file
115
crates/console/prometeu-bytecode/src/disassembler.rs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//! Deterministic disassembler for Prometeu Bytecode (PBC).
|
||||||
|
//!
|
||||||
|
//! Goals:
|
||||||
|
//! - Stable formatting across platforms (snapshot-friendly).
|
||||||
|
//! - Complete coverage of the Core ISA, including closures/coroutines.
|
||||||
|
//! - Roundtrip-safe with the paired `assembler` module.
|
||||||
|
//!
|
||||||
|
//! Format (one instruction per line):
|
||||||
|
//! - `MNEMONIC` for zero-operand instructions.
|
||||||
|
//! - `MNEMONIC <imm>` for 1-operand instructions (decimal unless stated).
|
||||||
|
//! - Special operand formats:
|
||||||
|
//! - `PUSH_F64 f64:0xhhhhhhhhhhhhhhhh` — exact IEEE-754 bits in hex (little-endian to_bits()).
|
||||||
|
//! - `MAKE_CLOSURE fn=<u32>, captures=<u32>`
|
||||||
|
//! - `SPAWN fn=<u32>, argc=<u32>`
|
||||||
|
//! - `CALL_CLOSURE argc=<u32>`
|
||||||
|
//! - `SYSCALL` is printed as `SYSCALL 0xhhhh` (numeric id in hex) to avoid cross-crate deps.
|
||||||
|
//!
|
||||||
|
//! Notes:
|
||||||
|
//! - All integers are printed in base-10 except where explicitly noted.
|
||||||
|
//! - Floats use exact bit-pattern format to prevent locale/rounding differences.
|
||||||
|
//! - Ordering is the canonical decode order; no address prefixes are emitted.
|
||||||
|
|
||||||
|
use crate::decode_next;
|
||||||
|
use crate::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
|
||||||
|
use crate::DecodeError;
|
||||||
|
|
||||||
|
fn fmt_f64_bits(bits: u64) -> String {
|
||||||
|
// Fixed-width 16 hex digits, lowercase.
|
||||||
|
format!("f64:0x{bits:016x}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_operand(op: CoreOpCode, imm: &[u8]) -> String {
|
||||||
|
match op {
|
||||||
|
CoreOpCode::Jmp | CoreOpCode::JmpIfFalse | CoreOpCode::JmpIfTrue => {
|
||||||
|
let v = u32::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
format!("{}", v)
|
||||||
|
}
|
||||||
|
CoreOpCode::PushI64 => {
|
||||||
|
let v = i64::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
format!("{}", v)
|
||||||
|
}
|
||||||
|
CoreOpCode::PushF64 => {
|
||||||
|
let v = u64::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
fmt_f64_bits(v)
|
||||||
|
}
|
||||||
|
CoreOpCode::PushBool => {
|
||||||
|
let v = imm[0];
|
||||||
|
format!("{}", v)
|
||||||
|
}
|
||||||
|
CoreOpCode::PushI32 => {
|
||||||
|
let v = i32::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
format!("{}", v)
|
||||||
|
}
|
||||||
|
CoreOpCode::PopN
|
||||||
|
| CoreOpCode::PushConst
|
||||||
|
| CoreOpCode::PushBounded
|
||||||
|
| CoreOpCode::GetGlobal
|
||||||
|
| CoreOpCode::SetGlobal
|
||||||
|
| CoreOpCode::GetLocal
|
||||||
|
| CoreOpCode::SetLocal
|
||||||
|
| CoreOpCode::Call
|
||||||
|
| CoreOpCode::Sleep => {
|
||||||
|
let v = u32::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
format!("{}", v)
|
||||||
|
}
|
||||||
|
CoreOpCode::MakeClosure => {
|
||||||
|
let fn_id = u32::from_le_bytes(imm[0..4].try_into().unwrap());
|
||||||
|
let cap = u32::from_le_bytes(imm[4..8].try_into().unwrap());
|
||||||
|
format!("fn={}, captures={}", fn_id, cap)
|
||||||
|
}
|
||||||
|
CoreOpCode::CallClosure => {
|
||||||
|
let argc = u32::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
format!("argc={}", argc)
|
||||||
|
}
|
||||||
|
CoreOpCode::Spawn => {
|
||||||
|
let fn_id = u32::from_le_bytes(imm[0..4].try_into().unwrap());
|
||||||
|
let argc = u32::from_le_bytes(imm[4..8].try_into().unwrap());
|
||||||
|
format!("fn={}, argc={}", fn_id, argc)
|
||||||
|
}
|
||||||
|
CoreOpCode::Syscall => {
|
||||||
|
let id = u32::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
// Hex id stable, avoids dependency on HAL metadata.
|
||||||
|
format!("0x{:04x}", id)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Fallback: raw immediate hex (little-endian, as encoded)
|
||||||
|
let mut s = String::with_capacity(2 + imm.len() * 2);
|
||||||
|
s.push_str("0x");
|
||||||
|
for b in imm {
|
||||||
|
use core::fmt::Write as _;
|
||||||
|
let _ = write!(&mut s, "{:02x}", b);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disassembles a contiguous byte slice (single function body) into deterministic text.
|
||||||
|
pub fn disassemble(bytes: &[u8]) -> Result<String, DecodeError> {
|
||||||
|
let mut pc = 0usize;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
while pc < bytes.len() {
|
||||||
|
let instr = decode_next(pc, bytes)?;
|
||||||
|
let name = instr.opcode.spec().name;
|
||||||
|
let imm_len = instr.opcode.spec().imm_bytes as usize;
|
||||||
|
if imm_len == 0 {
|
||||||
|
out.push(name.to_string());
|
||||||
|
} else {
|
||||||
|
let ops = format_operand(instr.opcode, instr.imm);
|
||||||
|
out.push(format!("{} {}", name, ops));
|
||||||
|
}
|
||||||
|
pc = instr.next_pc;
|
||||||
|
}
|
||||||
|
Ok(out.join("\n"))
|
||||||
|
}
|
||||||
@ -7,12 +7,16 @@ mod opcode_spec;
|
|||||||
mod program_image;
|
mod program_image;
|
||||||
mod value;
|
mod value;
|
||||||
pub mod isa; // canonical ISA boundary (core and future profiles)
|
pub mod isa; // canonical ISA boundary (core and future profiles)
|
||||||
|
mod disassembler;
|
||||||
|
mod assembler;
|
||||||
|
|
||||||
pub use abi::{
|
pub use abi::{
|
||||||
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC,
|
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC,
|
||||||
TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE,
|
TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE,
|
||||||
};
|
};
|
||||||
pub use decoder::{decode_next, DecodeError};
|
pub use decoder::{decode_next, DecodeError};
|
||||||
|
pub use disassembler::disassemble;
|
||||||
|
pub use assembler::{assemble, AsmError};
|
||||||
pub use layout::{compute_function_layouts, FunctionLayout};
|
pub use layout::{compute_function_layouts, FunctionLayout};
|
||||||
pub use model::{BytecodeLoader, FunctionMeta, LoadError};
|
pub use model::{BytecodeLoader, FunctionMeta, LoadError};
|
||||||
pub use program_image::ProgramImage;
|
pub use program_image::ProgramImage;
|
||||||
|
|||||||
42
crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs
Normal file
42
crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use prometeu_bytecode::{assemble, disassemble};
|
||||||
|
use prometeu_bytecode::isa::core::CoreOpCode;
|
||||||
|
|
||||||
|
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&(op as u16).to_le_bytes());
|
||||||
|
if let Some(bytes) = imm { out.extend_from_slice(bytes); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
|
||||||
|
// Program: PUSH_I32 7; MAKE_CLOSURE fn=1,captures=0; CALL_CLOSURE argc=1;
|
||||||
|
// SPAWN fn=2,argc=1; YIELD; SLEEP 3; SYSCALL 0x1003; FRAME_SYNC; HALT
|
||||||
|
let mut prog = Vec::new();
|
||||||
|
|
||||||
|
emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog);
|
||||||
|
// MAKE_CLOSURE (fn=1, captures=0)
|
||||||
|
let mut mc = [0u8; 8];
|
||||||
|
mc[0..4].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
mc[4..8].copy_from_slice(&0u32.to_le_bytes());
|
||||||
|
emit(CoreOpCode::MakeClosure, Some(&mc), &mut prog);
|
||||||
|
// CALL_CLOSURE argc=1
|
||||||
|
emit(CoreOpCode::CallClosure, Some(&1u32.to_le_bytes()), &mut prog);
|
||||||
|
// SPAWN (fn=2, argc=1)
|
||||||
|
let mut sp = [0u8; 8];
|
||||||
|
sp[0..4].copy_from_slice(&2u32.to_le_bytes());
|
||||||
|
sp[4..8].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
emit(CoreOpCode::Spawn, Some(&sp), &mut prog);
|
||||||
|
// YIELD
|
||||||
|
emit(CoreOpCode::Yield, None, &mut prog);
|
||||||
|
// SLEEP 3
|
||||||
|
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
|
||||||
|
// SYSCALL gfx.draw_line (0x1003)
|
||||||
|
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
|
||||||
|
// FRAME_SYNC
|
||||||
|
emit(CoreOpCode::FrameSync, None, &mut prog);
|
||||||
|
// HALT
|
||||||
|
emit(CoreOpCode::Halt, None, &mut prog);
|
||||||
|
|
||||||
|
let text = disassemble(&prog).expect("disasm ok");
|
||||||
|
let rebuilt = assemble(&text).expect("assemble ok");
|
||||||
|
assert_eq!(rebuilt, prog, "re-assembled bytes must match original");
|
||||||
|
}
|
||||||
39
crates/console/prometeu-bytecode/tests/disasm_snapshot.rs
Normal file
39
crates/console/prometeu-bytecode/tests/disasm_snapshot.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use prometeu_bytecode::disassemble;
|
||||||
|
use prometeu_bytecode::isa::core::CoreOpCode;
|
||||||
|
|
||||||
|
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&(op as u16).to_le_bytes());
|
||||||
|
if let Some(bytes) = imm { out.extend_from_slice(bytes); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_representative_program_is_stable() {
|
||||||
|
let mut prog = Vec::new();
|
||||||
|
emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog);
|
||||||
|
// MAKE_CLOSURE (fn=1, captures=0)
|
||||||
|
let mut mc = [0u8; 8];
|
||||||
|
mc[0..4].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
mc[4..8].copy_from_slice(&0u32.to_le_bytes());
|
||||||
|
emit(CoreOpCode::MakeClosure, Some(&mc), &mut prog);
|
||||||
|
// CALL_CLOSURE argc=1
|
||||||
|
emit(CoreOpCode::CallClosure, Some(&1u32.to_le_bytes()), &mut prog);
|
||||||
|
// SPAWN (fn=2, argc=1)
|
||||||
|
let mut sp = [0u8; 8];
|
||||||
|
sp[0..4].copy_from_slice(&2u32.to_le_bytes());
|
||||||
|
sp[4..8].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
emit(CoreOpCode::Spawn, Some(&sp), &mut prog);
|
||||||
|
// YIELD
|
||||||
|
emit(CoreOpCode::Yield, None, &mut prog);
|
||||||
|
// SLEEP 3
|
||||||
|
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
|
||||||
|
// SYSCALL 0x1003
|
||||||
|
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
|
||||||
|
// FRAME_SYNC
|
||||||
|
emit(CoreOpCode::FrameSync, None, &mut prog);
|
||||||
|
// HALT
|
||||||
|
emit(CoreOpCode::Halt, None, &mut prog);
|
||||||
|
|
||||||
|
let text = disassemble(&prog).expect("disasm ok");
|
||||||
|
let expected = "PUSH_I32 7\nMAKE_CLOSURE fn=1, captures=0\nCALL_CLOSURE argc=1\nSPAWN fn=2, argc=1\nYIELD\nSLEEP 3\nSYSCALL 0x1003\nFRAME_SYNC\nHALT";
|
||||||
|
assert_eq!(text, expected);
|
||||||
|
}
|
||||||
@ -1,61 +1,3 @@
|
|||||||
# PR-8.1 — Disassembler (Roundtrip + Snapshot Reliability)
|
|
||||||
|
|
||||||
## Briefing
|
|
||||||
|
|
||||||
The disassembler must be:
|
|
||||||
|
|
||||||
* Deterministic
|
|
||||||
* Complete
|
|
||||||
* Roundtrip-safe (encode → decode → encode)
|
|
||||||
* Snapshot-friendly
|
|
||||||
|
|
||||||
It must fully support:
|
|
||||||
|
|
||||||
* New opcodes (MAKE_CLOSURE, CALL_CLOSURE, SPAWN, YIELD, SLEEP)
|
|
||||||
* Updated syscall representation
|
|
||||||
* Closure and coroutine instructions
|
|
||||||
|
|
||||||
## Target
|
|
||||||
|
|
||||||
1. Update disassembler to support all new opcodes.
|
|
||||||
2. Guarantee stable formatting.
|
|
||||||
3. Add roundtrip validation tests.
|
|
||||||
|
|
||||||
Formatting rules:
|
|
||||||
|
|
||||||
* No unstable ordering.
|
|
||||||
* No implicit formatting differences across platforms.
|
|
||||||
* Explicit numeric representation where required.
|
|
||||||
|
|
||||||
## Acceptance Checklist
|
|
||||||
|
|
||||||
* [ ] All opcodes supported.
|
|
||||||
* [ ] Deterministic formatting.
|
|
||||||
* [ ] Roundtrip encode/decode test passes.
|
|
||||||
* [ ] Snapshot tests stable.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
1. Encode → disasm → reassemble → byte-equal check.
|
|
||||||
2. Snapshot test for representative programs.
|
|
||||||
3. Closure/coroutine disasm coverage.
|
|
||||||
|
|
||||||
## Junie Instructions
|
|
||||||
|
|
||||||
You MAY:
|
|
||||||
|
|
||||||
* Modify disassembler module.
|
|
||||||
* Add snapshot tests.
|
|
||||||
|
|
||||||
You MUST NOT:
|
|
||||||
|
|
||||||
* Change bytecode encoding format.
|
|
||||||
* Introduce formatting randomness.
|
|
||||||
|
|
||||||
If any opcode semantics unclear, STOP and ask.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# PR-8.3 — Layered Test Suite Architecture
|
# PR-8.3 — Layered Test Suite Architecture
|
||||||
|
|
||||||
## Briefing
|
## Briefing
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user