use prometeu_bytecode::decode_next; use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt}; fn encode_instr(op: CoreOpCode, imm: Option<&[u8]>) -> Vec { 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 } }