#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum DispatchTable {
Primary = 0,
Lead0 = 1,
Lead1 = 2,
Lead2 = 3,
Lead3 = 4,
Lead4 = 5,
}
#[derive(Debug, Clone, Copy)]
pub struct OpcodeInfo {
pub table: DispatchTable,
pub index: u8,
pub size: i8,
pub mnemonic: &'static str,
pub operand_format: &'static str,
pub pops: i8,
pub pushes: i8,
pub fpu_pops: u8,
pub fpu_push: u8,
pub fpu_inplace: bool,
pub mem_read: u8,
pub mem_write: u8,
pub category: &'static str,
pub semantics: OpcodeSemantics,
pub data_type: Option<PCodeDataType>,
}
impl OpcodeInfo {
#[inline]
pub fn is_implemented(&self) -> bool {
self.mnemonic != "InvalidExcode" && self.mnemonic != "Unknown" && self.size != 0
}
#[inline]
pub fn is_variable_length(&self) -> bool {
self.size < 0
}
#[inline]
pub fn touches_fpu(&self) -> bool {
self.fpu_pops > 0 || self.fpu_push > 0 || self.fpu_inplace
}
#[inline]
pub fn is_lead_byte(&self) -> bool {
matches!(
self.mnemonic,
"Lead0" | "Lead1" | "Lead2" | "Lead3" | "Lead4"
)
}
#[inline]
pub fn is_terminator(&self) -> bool {
matches!(
self.semantics,
OpcodeSemantics::Return | OpcodeSemantics::Branch { conditional: false }
)
}
#[inline]
pub fn is_call(&self) -> bool {
matches!(self.semantics, OpcodeSemantics::Call { .. })
}
}
pub static UNKNOWN_OPCODE: OpcodeInfo = OpcodeInfo {
table: DispatchTable::Primary,
index: 0,
size: 0,
mnemonic: "Unknown",
operand_format: "",
pops: 0,
pushes: 0,
fpu_pops: 0,
fpu_push: 0,
fpu_inplace: false,
mem_read: 0,
mem_write: 0,
category: "",
semantics: crate::pcode::semantics::OpcodeSemantics::Unclassified,
data_type: None,
};
include!(concat!(env!("OUT_DIR"), "/opcode_generated.rs"));
pub fn implemented_count() -> usize {
let tables: [&[OpcodeInfo; 256]; 6] = [
&PRIMARY_TABLE,
&LEAD0_TABLE,
&LEAD1_TABLE,
&LEAD2_TABLE,
&LEAD3_TABLE,
&LEAD4_TABLE,
];
tables
.iter()
.flat_map(|t| t.iter())
.filter(|o| o.is_implemented())
.count()
}
pub fn table_by_index(table: DispatchTable) -> &'static [OpcodeInfo; 256] {
match table {
DispatchTable::Primary => &PRIMARY_TABLE,
DispatchTable::Lead0 => &LEAD0_TABLE,
DispatchTable::Lead1 => &LEAD1_TABLE,
DispatchTable::Lead2 => &LEAD2_TABLE,
DispatchTable::Lead3 => &LEAD3_TABLE,
DispatchTable::Lead4 => &LEAD4_TABLE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_tables_have_256_entries() {
assert_eq!(PRIMARY_TABLE.len(), 256);
assert_eq!(LEAD0_TABLE.len(), 256);
assert_eq!(LEAD1_TABLE.len(), 256);
assert_eq!(LEAD2_TABLE.len(), 256);
assert_eq!(LEAD3_TABLE.len(), 256);
assert_eq!(LEAD4_TABLE.len(), 256);
}
#[test]
fn test_lead_byte_slots_in_primary() {
assert_eq!(PRIMARY_TABLE[0xFB].mnemonic, "Lead0");
assert_eq!(PRIMARY_TABLE[0xFC].mnemonic, "Lead1");
assert_eq!(PRIMARY_TABLE[0xFD].mnemonic, "Lead2");
assert_eq!(PRIMARY_TABLE[0xFE].mnemonic, "Lead3");
assert_eq!(PRIMARY_TABLE[0xFF].mnemonic, "Lead4");
}
#[test]
fn test_lead_bytes_size_is_1() {
for (i, entry) in PRIMARY_TABLE.iter().enumerate().skip(0xFB) {
assert_eq!(entry.size, 1, "Lead byte 0x{i:02X} should have size 1");
}
}
#[test]
fn test_known_primary_opcodes() {
assert_eq!(PRIMARY_TABLE[0x14].mnemonic, "ExitProc");
assert_eq!(PRIMARY_TABLE[0x14].size, 1);
assert_eq!(PRIMARY_TABLE[0x1E].mnemonic, "Branch");
assert_eq!(PRIMARY_TABLE[0x1E].size, 3);
assert_eq!(PRIMARY_TABLE[0xF3].mnemonic, "LitI2");
assert_eq!(PRIMARY_TABLE[0xF3].size, 3);
assert_eq!(PRIMARY_TABLE[0xF5].mnemonic, "LitI4");
assert_eq!(PRIMARY_TABLE[0xF5].size, 5);
assert_eq!(PRIMARY_TABLE[0xA9].mnemonic, "AddI2");
assert_eq!(PRIMARY_TABLE[0xA9].size, 1);
}
#[test]
fn test_variable_length_opcodes() {
assert_eq!(PRIMARY_TABLE[0x29].mnemonic, "FFreeAd");
assert!(PRIMARY_TABLE[0x29].is_variable_length());
assert_eq!(PRIMARY_TABLE[0x32].mnemonic, "FFreeStr");
assert!(PRIMARY_TABLE[0x32].is_variable_length());
assert_eq!(PRIMARY_TABLE[0x36].mnemonic, "FFreeVar");
assert!(PRIMARY_TABLE[0x36].is_variable_length());
}
#[test]
fn test_lookup_primary() {
let (info, consumed) = lookup(0x14, None);
assert_eq!(info.mnemonic, "ExitProc");
assert_eq!(consumed, 1);
}
#[test]
fn test_lookup_lead0() {
let (info, consumed) = lookup(0xFB, Some(0x00));
assert_eq!(consumed, 2);
assert_eq!(info.table, DispatchTable::Lead0);
}
#[test]
fn test_lookup_lead4() {
let (info, consumed) = lookup(0xFF, Some(0x10));
assert_eq!(consumed, 2);
assert_eq!(info.table, DispatchTable::Lead4);
}
#[test]
fn test_implemented_count() {
let count = implemented_count();
assert!(count > 1000, "Expected >1000 named opcodes, got {count}");
assert!(count < 1300, "Expected <1300 named opcodes, got {count}");
}
#[test]
fn test_is_implemented() {
assert!(PRIMARY_TABLE[0x14].is_implemented()); assert!(!PRIMARY_TABLE[0x01].is_implemented()); }
#[test]
fn test_is_lead_byte() {
assert!(PRIMARY_TABLE[0xFB].is_lead_byte());
assert!(!PRIMARY_TABLE[0x14].is_lead_byte());
}
#[test]
fn test_is_terminator() {
assert!(PRIMARY_TABLE[0x14].is_terminator());
assert!(PRIMARY_TABLE[0x1E].is_terminator());
assert!(!PRIMARY_TABLE[0x1C].is_terminator());
assert!(!PRIMARY_TABLE[0x1D].is_terminator());
assert!(!PRIMARY_TABLE[0xA9].is_terminator());
assert!(!PRIMARY_TABLE[0x04].is_terminator());
}
#[test]
fn test_is_call() {
let any_primary_call = PRIMARY_TABLE.iter().any(|o| o.is_call());
let any_lead3_call = LEAD3_TABLE.iter().any(|o| o.is_call());
assert!(
any_primary_call || any_lead3_call,
"expected at least one Call opcode across primary+lead3 tables"
);
assert!(!PRIMARY_TABLE[0x14].is_call());
assert!(!PRIMARY_TABLE[0xA9].is_call());
assert!(!PRIMARY_TABLE[0x1E].is_call());
}
#[test]
fn test_terminator_and_call_are_disjoint() {
let tables: [&[OpcodeInfo; 256]; 6] = [
&PRIMARY_TABLE,
&LEAD0_TABLE,
&LEAD1_TABLE,
&LEAD2_TABLE,
&LEAD3_TABLE,
&LEAD4_TABLE,
];
for entry in tables.iter().flat_map(|t| t.iter()) {
assert!(
!(entry.is_terminator() && entry.is_call()),
"{} is both terminator and call",
entry.mnemonic
);
}
}
#[test]
fn test_table_by_index() {
let primary = table_by_index(DispatchTable::Primary);
assert_eq!(primary[0x14].mnemonic, "ExitProc");
let lead0 = table_by_index(DispatchTable::Lead0);
assert_eq!(lead0.len(), 256);
}
#[test]
fn test_operand_format_specifiers_normalized() {
assert_eq!(PRIMARY_TABLE[0xF3].operand_format, "%2");
assert_eq!(PRIMARY_TABLE[0x1E].operand_format, "%l");
assert_eq!(PRIMARY_TABLE[0x04].operand_format, "%a");
assert_eq!(PRIMARY_TABLE[0x1B].operand_format, "%s");
}
#[test]
fn test_semantics_fields_populated() {
assert_eq!(PRIMARY_TABLE[0xA9].pops, 2);
assert_eq!(PRIMARY_TABLE[0xA9].pushes, 1);
assert_eq!(PRIMARY_TABLE[0xA9].category, "arith");
assert_eq!(PRIMARY_TABLE[0x04].pops, 0);
assert_eq!(PRIMARY_TABLE[0x04].pushes, 1);
assert_eq!(PRIMARY_TABLE[0x04].mem_read, 4);
assert_eq!(PRIMARY_TABLE[0x04].category, "load_frame");
assert_eq!(PRIMARY_TABLE[0x72].pops, 2);
assert_eq!(PRIMARY_TABLE[0x72].pushes, 0);
assert_eq!(PRIMARY_TABLE[0x72].mem_write, 8);
assert_eq!(PRIMARY_TABLE[0x6E].fpu_push, 1);
assert_eq!(PRIMARY_TABLE[0x6E].pops, 0);
assert_eq!(PRIMARY_TABLE[0x6E].pushes, 0);
assert_eq!(PRIMARY_TABLE[0x74].fpu_pops, 1);
assert_eq!(PRIMARY_TABLE[0x01].pops, 0);
assert_eq!(PRIMARY_TABLE[0x01].pushes, 0);
assert_eq!(PRIMARY_TABLE[0x01].category, "");
assert_eq!(LEAD0_TABLE[0x94].mnemonic, "AddVar");
assert_eq!(LEAD0_TABLE[0x94].pops, 4);
assert_eq!(LEAD0_TABLE[0x94].pushes, 4);
assert_eq!(LEAD0_TABLE[0x94].category, "arith");
assert_eq!(LEAD1_TABLE[0x00].fpu_pops, 1);
assert_eq!(LEAD1_TABLE[0x00].pushes, 1);
assert_eq!(LEAD1_TABLE[0x00].category, "convert");
}
#[test]
fn test_all_opcodes_have_valid_table_field() {
let tables: [(DispatchTable, &[OpcodeInfo; 256]); 6] = [
(DispatchTable::Primary, &PRIMARY_TABLE),
(DispatchTable::Lead0, &LEAD0_TABLE),
(DispatchTable::Lead1, &LEAD1_TABLE),
(DispatchTable::Lead2, &LEAD2_TABLE),
(DispatchTable::Lead3, &LEAD3_TABLE),
(DispatchTable::Lead4, &LEAD4_TABLE),
];
for (expected_table, table) in &tables {
for entry in table.iter() {
assert_eq!(
entry.table, *expected_table,
"Opcode 0x{:02X} in {:?} has wrong table field {:?}",
entry.index, expected_table, entry.table
);
}
}
}
}