superh 0.2.1

Disassembler for the SuperH (SH) instruction set (SH1/2/3/4)
Documentation
use superh::{
    Address, Architecture, BranchDisp8, BranchDisp12, DecodeOptions, DecodeResult, Disp, FormatIns,
    FormatOptions, ImmediateRadix, Ins, Opcode, OpcodeId, Reg, StringFormatter, decode,
};

fn options(architecture: Architecture) -> DecodeOptions {
    DecodeOptions::new(architecture)
}

fn instruction(word: u16, architecture: Architecture) -> Ins {
    decode(word, &options(architecture)).instruction().copied().expect("known instruction")
}

fn assert_copy<T: Copy>() {}

#[test]
fn decodes_valid_instruction_without_location() {
    let instruction = instruction(0x6323, Architecture::default());
    assert_eq!(instruction, Ins::MovRmRn { rn: Reg::R3, rm: Reg::R2 });
    assert_eq!(instruction.opcode(), Opcode::MovRmRn);
    assert_eq!(instruction.encode(), Some(0x6323));
}

#[test]
fn decoded_values_are_copy_and_compact() {
    assert_copy::<Ins>();
    assert_copy::<DecodeResult>();
    assert_eq!(core::mem::size_of::<Ins>(), 4);
    assert_eq!(core::mem::size_of::<DecodeResult>(), 4);
}

#[test]
fn preserves_raw_pc_relative_displacement() {
    let a = instruction(0xd001, Architecture::default());
    let b = instruction(0xd001, Architecture::default());
    assert_eq!(a, b);
    let Ins::MovlAtDispPcRn { disp, .. } = a else { panic!("wrong opcode") };
    assert_eq!(disp.value(), 1);
}

#[test]
fn reports_unknown_word_separately() {
    assert_eq!(decode(0xffff, &options(Architecture::default())), DecodeResult::Unknown(0xffff));
}

#[test]
fn stable_opcode_ids_round_trip() {
    let instruction = instruction(0x6323, Architecture::default());
    let id = instruction.opcode().id();
    assert_eq!(id.value(), 0);
    assert_eq!(Opcode::from_id(id), Some(Opcode::MovRmRn));
    assert_eq!(Opcode::from_id(OpcodeId::new(u16::MAX)), None);
}

#[test]
fn location_aware_display_resolves_pc_relative_address() {
    let instruction = instruction(0xd001, Architecture::default());
    assert_eq!(
        instruction.at(0x1002).display(&FormatOptions::default()).to_string(),
        "mov.l @(0x1008, pc), r0"
    );
}

#[test]
fn formatting_options_do_not_affect_decode() {
    let instruction = instruction(0xe0ff, Architecture::default());
    let decimal = FormatOptions::new(ImmediateRadix::Decimal);
    assert_eq!(instruction.at(0).display(&decimal).to_string(), "mov #-1, r0");
    assert_eq!(instruction.at(0).display(&FormatOptions::default()).to_string(), "mov #-0x1, r0");
}

#[test]
fn signed_formatter_handles_the_full_i32_range() {
    let mut formatter = StringFormatter::new(FormatOptions::default());
    formatter.write_simm(i32::MIN).expect("string formatting");
    assert_eq!(formatter.into_string(), "-0x80000000");
}

#[test]
fn decode_results_support_reusable_formatting_buffers() {
    let options = FormatOptions::default();
    let mut formatter = StringFormatter::new(options);

    let instruction = decode(0x6323, &DecodeOptions::default());
    instruction.write_at(&mut formatter, 0x1000).expect("format instruction");
    assert_eq!(formatter.as_str(), "mov r2, r3");

    formatter.clear();
    let unknown = DecodeResult::Unknown(0xffff);
    unknown.write_at(&mut formatter, 0x1002).expect("format unknown word");
    assert_eq!(formatter.as_str(), ".word 0xffff");
}

#[test]
fn public_operand_construction_preserves_valid_ranges() {
    assert_eq!(Reg::from_number(15), Some(Reg::R15));
    assert_eq!(Reg::from_number(16), None);
    assert_eq!(Disp::new(0xab), Disp::from(0xab));
    assert_eq!(BranchDisp8::new(-128), BranchDisp8::from(-128));
    assert_eq!(BranchDisp12::new(-2048).expect("lower bound").value(), -2048);
    assert_eq!(BranchDisp12::new(2047).expect("upper bound").value(), 2047);
    assert_eq!(BranchDisp12::new(-2049), None);
    assert_eq!(BranchDisp12::new(2048), None);
    assert_eq!(u32::from(Address::from(0x8c01_0000)), 0x8c01_0000);
}

#[test]
fn decode_is_available_in_const_contexts() {
    const OPTIONS: DecodeOptions = DecodeOptions::new(Architecture::Sh1);
    const RESULT: DecodeResult = decode(0x0009, &OPTIONS);
    const KNOWN_RESULT: Option<Ins> = Opcode::Nop.decode(0x0009, &OPTIONS);
    const ENCODED: Option<u16> = Ins::Nop.encode();
    assert_eq!(RESULT.instruction().map(Ins::opcode), Some(Opcode::Nop));
    assert_eq!(KNOWN_RESULT, RESULT.instruction().copied());
    assert_eq!(ENCODED, Some(0x0009));
}

#[cfg(feature = "sh3")]
#[test]
fn banked_register_construction_validates_its_range() {
    use superh::BankReg;

    assert_eq!(BankReg::from_number(7).expect("upper bound").number(), 7);
    assert_eq!(BankReg::from_number(8), None);
}

#[cfg(feature = "sh4")]
#[test]
fn floating_register_construction_validates_architectural_numbers() {
    use superh::{DReg, FReg, VecReg};

    assert_eq!(FReg::from_number(15), Some(FReg::Fr15));
    assert_eq!(FReg::from_number(16), None);
    assert_eq!(DReg::from_number(14), Some(DReg::Dr14));
    assert_eq!(DReg::from_number(3), None);
    assert_eq!(VecReg::from_number(12), Some(VecReg::Fv12));
    assert_eq!(VecReg::from_number(6), None);
}

#[test]
fn representative_manual_encodings_decode() {
    let cases = [
        (0x0009, "nop"),
        (0x321c, "add r1, r2"),
        (0xc320, "trapa #0x20"),
        (0x4e2b, "jmp @r14"),
        (0x000b, "rts"),
    ];
    for (word, expected) in cases {
        let instruction = instruction(word, Architecture::default());
        assert_eq!(instruction.at(0).display(&FormatOptions::default()).to_string(), expected);
    }
}

#[cfg(feature = "sh4")]
#[test]
fn sh4_fmov_mnemonic_distinguishes_memory_and_register_forms() {
    let cases = [
        (0xf3e6, "fmov.s @(r0, r14), fr3"),
        (0xf3e8, "fmov.s @r14, fr3"),
        (0xf3e9, "fmov.s @r14+, fr3"),
        (0xf3e7, "fmov.s fr14, @(r0, r3)"),
        (0xf3ea, "fmov.s fr14, @r3"),
        (0xf3eb, "fmov.s fr14, @-r3"),
        (0xf3ec, "fmov fr14, fr3"),
    ];
    for (word, expected) in cases {
        let instruction = instruction(word, Architecture::Sh4);
        assert_eq!(instruction.at(0).display(&FormatOptions::default()).to_string(), expected);
    }
}

#[cfg(feature = "sh2")]
#[test]
fn delayed_conditional_branches_use_manual_mnemonics() {
    let cases = [(0x8d00, "bt/s 0x4"), (0x8f00, "bf/s 0x4")];
    for (word, expected) in cases {
        let instruction = instruction(word, Architecture::Sh2);
        assert_eq!(instruction.at(0).display(&FormatOptions::default()).to_string(), expected);
    }
}

#[test]
fn every_word_has_deterministic_opcode_identity() {
    let options = options(Architecture::default());
    for word in 0u16..=u16::MAX {
        let first = decode(word, &options);
        let second = decode(word, &options);
        assert_eq!(first, second, "word 0x{word:04x}");
        if let DecodeResult::Instruction(instruction) = first {
            let opcode = instruction.opcode();
            assert_eq!(Opcode::from_id(opcode.id()), Some(opcode));
        }
    }
}

#[test]
fn known_opcode_decode_matches_full_decode_for_every_valid_word() {
    let architectures = [
        #[cfg(feature = "sh1")]
        Architecture::Sh1,
        #[cfg(feature = "sh2")]
        Architecture::Sh2,
        #[cfg(feature = "sh3")]
        Architecture::Sh3,
        #[cfg(feature = "sh4")]
        Architecture::Sh4,
    ];

    for architecture in architectures {
        let options = options(architecture);
        for word in 0u16..=u16::MAX {
            let decoded = decode(word, &options);
            if let DecodeResult::Instruction(instruction) = decoded {
                assert_eq!(
                    instruction.opcode().decode(word, &options),
                    Some(instruction),
                    "word 0x{word:04x} on {architecture:?}"
                );
            }
        }
    }
}

#[test]
fn every_valid_encoding_round_trips_exactly() {
    let architectures = [
        #[cfg(feature = "sh1")]
        Architecture::Sh1,
        #[cfg(feature = "sh2")]
        Architecture::Sh2,
        #[cfg(feature = "sh3")]
        Architecture::Sh3,
        #[cfg(feature = "sh4")]
        Architecture::Sh4,
    ];

    for architecture in architectures {
        let options = options(architecture);
        for word in 0u16..=u16::MAX {
            let decoded = decode(word, &options);
            if let DecodeResult::Instruction(instruction) = decoded {
                let encoded = instruction.encode().expect("decoded operands fit their encoding");
                assert_eq!(encoded, word, "word 0x{word:04x} on {architecture:?}");
                assert_eq!(decode(encoded, &options), decoded);
            }
        }
    }
}

#[test]
fn encode_rejects_a_displacement_that_exceeds_its_field() {
    let invalid = Ins::MovbR0AtDispRn { rn: Reg::R0, disp: Disp::new(0x10) };
    assert_eq!(invalid.encode(), None);

    let valid = Ins::MovbR0AtDispRn { rn: Reg::R0, disp: Disp::new(0x0f) };
    assert_eq!(valid.encode(), Some(0x800f));

    let wide = Ins::Mova { disp: Disp::new(0xff) };
    assert_eq!(wide.encode(), Some(0xc7ff));
}

#[test]
fn known_opcode_decode_rejects_a_mismatched_word() {
    let options = options(Architecture::default());
    assert_eq!(Opcode::Nop.decode(0x6323, &options), None);
}

#[cfg(all(feature = "sh1", feature = "sh4"))]
#[test]
fn known_opcode_decode_rejects_an_unavailable_architecture() {
    let word = 0xf3ec;
    assert!(matches!(
        decode(word, &options(Architecture::Sh4)),
        DecodeResult::Instruction(Ins::FmovFrmFrn { .. })
    ));
    assert_eq!(Opcode::FmovFrmFrn.decode(word, &options(Architecture::Sh1)), None);
}

#[test]
fn every_available_opcode_is_reachable_and_filtered_by_architecture() {
    let architectures = [
        #[cfg(feature = "sh1")]
        Architecture::Sh1,
        #[cfg(feature = "sh2")]
        Architecture::Sh2,
        #[cfg(feature = "sh3")]
        Architecture::Sh3,
        #[cfg(feature = "sh4")]
        Architecture::Sh4,
    ];

    for architecture in architectures {
        let options = options(architecture);
        let mut reached = vec![false; usize::from(u16::MAX) + 1];
        for word in 0u16..=u16::MAX {
            if let DecodeResult::Instruction(instruction) = decode(word, &options) {
                let opcode = instruction.opcode();
                assert!(opcode.architectures().contains(architecture));
                reached[usize::from(opcode.id().value())] = true;
            }
        }
        for raw_id in 0u16..=u16::MAX {
            let Some(opcode) = Opcode::from_id(OpcodeId::new(raw_id)) else { continue };
            assert_eq!(
                reached[usize::from(raw_id)],
                opcode.architectures().contains(architecture),
                "opcode {opcode:?} on {architecture:?}"
            );
        }
    }
}