2026-03-24 13:40:45 +00:00

161 lines
6.2 KiB
Rust

use prometeu_bytecode::decode_next;
use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
fn encode_instr(op: CoreOpCode, imm: Option<&[u8]>) -> Vec<u8> {
let mut out = Vec::new();
let code = op as u16;
out.extend_from_slice(&code.to_le_bytes());
let spec = op.spec();
let need = spec.imm_bytes as usize;
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => {
panic!("immediate size mismatch for {:?}: expected {}, got {}", op, n, bytes.len())
}
(n, None) => panic!("missing immediate for {:?}: need {} bytes", op, n),
}
out
}
fn disasm(bytes: &[u8]) -> String {
// Minimal test-only disasm: NAME [operands]
let mut pc = 0usize;
let mut lines = Vec::new();
while pc < bytes.len() {
match decode_next(pc, bytes) {
Ok(instr) => {
let name = instr.opcode.spec().name;
let mut line = String::from(name);
let imm_len = instr.opcode.spec().imm_bytes as usize;
if imm_len > 0 {
// Heuristic formatting based on known op immediates
line.push(' ');
let s = match instr.opcode {
CoreOpCode::Jmp | CoreOpCode::JmpIfFalse | CoreOpCode::JmpIfTrue => {
format!("{}", instr.imm_u32().unwrap())
}
CoreOpCode::PushI64 => format!("{}", instr.imm_i64().unwrap()),
CoreOpCode::PushF64 => format!("{}", instr.imm_f64().unwrap()),
CoreOpCode::PushBool => format!("{}", instr.imm_u8().unwrap()),
CoreOpCode::PushI32 => format!("{}", instr.imm_i32().unwrap()),
CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
format!("{}", instr.imm_u32().unwrap())
}
CoreOpCode::Syscall | CoreOpCode::Intrinsic => {
format!("0x{}", hex::encode(instr.imm))
}
_ => format!("0x{}", hex::encode(instr.imm)),
};
line.push_str(&s);
}
lines.push(line);
pc = instr.next_pc;
}
Err(_) => break,
}
}
lines.join("\n")
}
#[test]
fn encode_decode_roundtrip_preserves_structure() {
// Program: PUSH_I32 42; PUSH_I32 100; ADD; PUSH_BOOL 1; JMP 12; NOP; HALT
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::PushI32, Some(&42i32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::PushI32, Some(&100i32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Add, None));
prog.extend(encode_instr(CoreOpCode::PushBool, Some(&[1u8])));
// Jump to the HALT (compute absolute PC within this byte slice)
// Current pc after previous: 2+4 + 2+4 + 2 + 2+1 = 17 bytes
// Next we place: JMP (2+4), NOP (2), HALT (2)
// We want JMP target to land at the HALT's pc
let jmp_target: u32 = 17 + 2 + 4 + 2; // pc where HALT starts
prog.extend(encode_instr(CoreOpCode::Jmp, Some(&jmp_target.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Nop, None));
prog.extend(encode_instr(CoreOpCode::Halt, None));
// Decode sequentially and check opcodes and immediates
let mut pc = 0usize;
let mut seen = Vec::new();
while pc < prog.len() {
let instr = decode_next(pc, &prog).expect("decode ok");
seen.push(instr);
pc = instr.next_pc;
}
assert_eq!(seen.len(), 7);
assert_eq!(seen[0].opcode, CoreOpCode::PushI32);
assert_eq!(seen[0].imm_i32().unwrap(), 42);
assert_eq!(seen[1].opcode, CoreOpCode::PushI32);
assert_eq!(seen[1].imm_i32().unwrap(), 100);
assert_eq!(seen[2].opcode, CoreOpCode::Add);
assert_eq!(seen[3].opcode, CoreOpCode::PushBool);
assert_eq!(seen[3].imm_u8().unwrap(), 1);
assert_eq!(seen[4].opcode, CoreOpCode::Jmp);
assert_eq!(seen[4].imm_u32().unwrap(), jmp_target);
assert_eq!(seen[5].opcode, CoreOpCode::Nop);
assert_eq!(seen[6].opcode, CoreOpCode::Halt);
}
#[test]
fn disasm_contains_expected_mnemonics_and_operands() {
// Tiny deterministic sample: NOP; PUSH_I32 -7; PUSH_BOOL 0; ADD; HALT
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Nop, None));
prog.extend(encode_instr(CoreOpCode::PushI32, Some(&(-7i32).to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::PushBool, Some(&[0u8])));
prog.extend(encode_instr(CoreOpCode::Add, None));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = disasm(&prog);
// Must contain stable opcode names and operand text
assert!(text.contains("NOP"));
assert!(text.contains("PUSH_I32 -7"));
assert!(text.contains("PUSH_BOOL 0"));
assert!(text.contains("ADD"));
assert!(text.contains("HALT"));
}
#[test]
fn hostcall_roundtrips_with_decimal_index() {
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Hostcall, Some(&7u32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = disasm(&prog);
assert!(text.contains("HOSTCALL 7"));
let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble hostcall");
assert_eq!(rebuilt, prog);
}
#[test]
fn intrinsic_roundtrips_with_hex_id() {
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Intrinsic, Some(&0x1000u32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = prometeu_bytecode::disassemble(&prog).expect("disasm intrinsic");
assert!(text.contains("INTRINSIC 0x1000"));
let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble intrinsic");
assert_eq!(rebuilt, prog);
}
// Minimal hex helper to avoid extra deps in tests
mod hex {
pub fn encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
const HEX: &[u8; 16] = b"0123456789abcdef";
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}
}