128 lines
5.1 KiB
Rust
128 lines
5.1 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::PushBounded => {
|
|
format!("{}", instr.imm_u32().unwrap())
|
|
}
|
|
_ => 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"));
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|