disc-v 0.1.1

A RISC-V disassembler
Documentation
// Copyright (c) 2026 Christopher Swenson
// Licensed under the Apache-2.0 license.
//
// Derived from riscv-disassembler, Copyright (c) 2016-2017 Michael Clark
// and Copyright (c) 2017-2018 SiFive, Inc., under the MIT license; see NOTICE.

//! Opcode tables: one static [`Opcode`] per instruction, grouped by extension.

pub(crate) mod a;
pub(crate) mod b;
pub(crate) mod c;
pub(crate) mod d;
pub(crate) mod f;
pub(crate) mod h;
pub(crate) mod i;
pub(crate) mod m;
pub(crate) mod pseudo;
pub(crate) mod q;
pub(crate) mod system;
pub(crate) mod v;
pub(crate) mod zawrs;
pub(crate) mod zcb;
pub(crate) mod zfa;
pub(crate) mod zfh;
pub(crate) mod zicbo;
pub(crate) mod zicond;
pub(crate) mod zimop;

use crate::Isa;

/// How an instruction's operands are laid out in its encoding.
///
/// The names follow the instruction formats in the RISC-V specification
/// (R, I, S, SB, U, UJ, and the compressed CR, CI, CSS, CIW, CL, CS, CB, CJ),
/// with a suffix where a format has several immediate layouts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Codec {
    Li,
    CssSqsp,
    CssSdsp,
    CssSwsp,
    CsSq,
    CsSd,
    CsSw,
    Cs,
    CrJr,
    CrJalr,
    CrMv,
    Cr,
    ClLq,
    ClLd,
    ClLw,
    /// Zcb byte load: rd', rs1' and a 2-bit unsigned offset.
    ClB,
    /// Zcb halfword load: rd', rs1' and a 2-byte-aligned 2-bit offset.
    ClH,
    /// Zcb byte store: rs2', rs1' and a 2-bit unsigned offset.
    CsB,
    /// Zcb halfword store: rs2', rs1' and a 2-byte-aligned 2-bit offset.
    CsH,
    /// Zcb unary operation on rd' (which is also rs1').
    CuRd,
    CjJal,
    Cj,
    Ciw4spn,
    CiNone,
    CiLui,
    CiLi,
    CiLqsp,
    CiLdsp,
    CiLwsp,
    Ci16sp,
    CiSh6,
    Ci,
    CbSh6,
    CbImm,
    Cb,
    RF,
    RL,
    RA,
    R4M,
    RM,
    R,
    SB,
    S,
    ICsr,
    ISh7,
    ISh6,
    ISh5,
    I,
    Uj,
    U,
    None,
    Illegal,
    /// Vector arithmetic, load or store: vd/rd, vs1/rs1, vs2/rs2, the `vm`
    /// mask bit, and a signed 5-bit immediate in the vs1 field.
    V,
    /// As [`Codec::V`], with an unsigned 5-bit immediate.
    Vu,
    /// As [`Codec::V`], with an unsigned 6-bit immediate in bit 26 and the
    /// vs1 field (`vror.vi`).
    Vu6,
    /// `vsetvli`: rd, rs1 and an 11-bit `vtype` immediate.
    VsetVli,
    /// `vsetivli`: rd, a 5-bit AVL immediate in the rs1 field, and a 10-bit
    /// `vtype` immediate.
    VsetIvli,
}

/// Operand format strings.
///
/// Each character expands to one piece of the operand text:
///
/// | Char | Output |
/// |------|--------|
/// | `0` `1` `2` | integer register `rd`, `rs1`, `rs2` |
/// | `3` `4` `5` `6` | floating-point register `rd`, `rs1`, `rs2`, `rs3` |
/// | `7` | `rs1` field as an unsigned immediate (CSR `zimm`) |
/// | `i` | immediate, in decimal |
/// | `>` | shift amount, in hex |
/// | `u` | upper immediate (bits 31:12), in hex |
/// | `o` | target address of a PC-relative offset |
/// | `c` | CSR name |
/// | `r` | `,` and the rounding mode, unless it is dynamic |
/// | `R` | `,` and the rounding mode, unless it is RNE (for exact conversions) |
/// | `p` `s` | fence predecessor and successor sets |
/// | `F` | the `fli` constant selected by the rs1 field |
/// | `D` `A` `B` | vector register `vd` (or `vs3`), `vs1`, `vs2` |
/// | `M` | `,v0.t` if the vector instruction is masked |
/// | `Z` | `v0` (the carry or merge operand) |
/// | `T` | the `vtype` immediate of `vsetvli`/`vsetivli` |
/// | `P` | prefetch offset: the immediate with its low five bits cleared |
/// | `l` | length of the encoding in bytes |
/// | `x` | the encoding, in hex, padded to a whole number of 16-bit parcels |
/// | `,` ` ` `(` `)` | themselves |
pub(crate) mod fmt {
    pub(crate) const NONE: &str = "";
    pub(crate) const RS1: &str = "1";
    pub(crate) const OFFSET: &str = "o";
    pub(crate) const PRED_SUCC: &str = "p,s";
    pub(crate) const RS1_RS2: &str = "1,2";
    pub(crate) const RD_IMM: &str = "0,i";
    pub(crate) const RD_UIMM: &str = "0,u";
    pub(crate) const RD_OFFSET: &str = "0,o";
    pub(crate) const RD_RS1_RS2: &str = "0,1,2";
    pub(crate) const FRD_RS1: &str = "3,1";
    pub(crate) const FRD_FRS1: &str = "3,4";
    pub(crate) const RD_FRS1: &str = "0,4";
    pub(crate) const RD_FRS1_FRS2: &str = "0,4,5";
    pub(crate) const FRD_FRS1_FRS2: &str = "3,4,5";
    pub(crate) const RM_FRD_FRS1: &str = "3,4r";
    pub(crate) const RM_FRD_RS1: &str = "3,1r";
    pub(crate) const RM_RD_FRS1: &str = "0,4r";
    pub(crate) const RM_FRD_FRS1_FRS2: &str = "3,4,5r";
    pub(crate) const RM_FRD_FRS1_FRS2_FRS3: &str = "3,4,5,6r";
    pub(crate) const RD_RS1_IMM: &str = "0,1,i";
    pub(crate) const RD_RS1_SHAMT: &str = "0,1,>";
    pub(crate) const RD_OFFSET_RS1: &str = "0,i(1)";
    pub(crate) const FRD_OFFSET_RS1: &str = "3,i(1)";
    pub(crate) const RD_CSR_RS1: &str = "0,c,1";
    pub(crate) const RD_CSR_ZIMM: &str = "0,c,7";
    pub(crate) const RS2_OFFSET_RS1: &str = "2,i(1)";
    pub(crate) const FRS2_OFFSET_RS1: &str = "5,i(1)";
    pub(crate) const RS1_RS2_OFFSET: &str = "1,2,o";
    pub(crate) const RD_RS2_ADDR_RS1: &str = "0,2,(1)";
    pub(crate) const RD_ADDR_RS1: &str = "0,(1)";
    pub(crate) const RD: &str = "0";
    pub(crate) const RD_ZIMM: &str = "0,7";
    pub(crate) const RD_RS1: &str = "0,1";
    pub(crate) const RD_RS2: &str = "0,2";
    pub(crate) const RS1_OFFSET: &str = "1,o";
    pub(crate) const RS2_OFFSET: &str = "2,o";
    pub(crate) const OFFSET_RS1: &str = "i(1)";
    pub(crate) const RD_SHAMT: &str = "0,>";
    pub(crate) const RD_CSR: &str = "0,c";
    pub(crate) const CSR_RS1: &str = "c,1";
    pub(crate) const CSR_ZIMM: &str = "c,7";
    pub(crate) const IMM: &str = "i";
    pub(crate) const WIDEN_FRD_FRS1: &str = "3,4R";
    pub(crate) const WIDEN_FRD_RS1: &str = "3,1R";
    pub(crate) const INSN: &str = "l, x";
    pub(crate) const RAW: &str = "x";
    pub(crate) const ADDR_RS1: &str = "(1)";
    pub(crate) const RS2: &str = "2";
    pub(crate) const RS2_ADDR_RS1: &str = "2,(1)";
    pub(crate) const UIMM: &str = "u";
    pub(crate) const FRD_FLI: &str = "3,F";
    pub(crate) const FRD_RS1_RS2: &str = "3,1,2";
    pub(crate) const PREFETCH: &str = "P(1)";
}

/// A condition on decoded operands, used to pick a pseudoinstruction.
#[derive(Clone, Copy, Debug)]
pub(crate) enum Constraint {
    RdEq(u8),
    Rs1Eq(u8),
    Rs2Eq(u8),
    Rs2EqRs1,
    RdEqRs1,
    ImmEq(i32),
    /// The immediate's bits in the mask (first) equal the value (second).
    ImmMaskEq(i32, i32),
    /// The CSR number, which is held in the immediate.
    CsrEq(i32),
}

/// An alternative way to show an instruction when all of `when` hold.
pub(crate) struct Pseudo {
    pub(crate) op: &'static Opcode,
    pub(crate) when: &'static [Constraint],
}

impl Pseudo {
    pub(crate) const fn new(op: &'static Opcode, when: &'static [Constraint]) -> Self {
        Pseudo { op, when }
    }
}

/// An opcode that is decoded by matching the encoding against a mask, as the
/// vector instructions are.
pub(crate) struct MaskedOpcode {
    pub(crate) bits: u32,
    pub(crate) mask: u32,
    pub(crate) op: Opcode,
}

/// Builds a [`MaskedOpcode`].
pub(crate) const fn masked(bits: u32, mask: u32, op: Opcode) -> MaskedOpcode {
    MaskedOpcode { bits, mask, op }
}

/// Returns the first opcode in `table` that matches `inst`.
pub(crate) fn lookup_masked(table: &'static [MaskedOpcode], inst: u64) -> Option<&'static Opcode> {
    let inst = inst as u32;
    table
        .iter()
        .find(|e| inst & e.mask == e.bits)
        .map(|e| &e.op)
}

/// One entry in the opcode tables.
pub(crate) struct Opcode {
    pub(crate) name: &'static str,
    pub(crate) codec: Codec,
    /// Operand format string; see [`fmt`].
    pub(crate) format: &'static str,
    /// Pseudoinstructions to try, in order; the first match is shown instead.
    pub(crate) pseudo: &'static [Pseudo],
    /// For compressed instructions, the expansion for RV32, RV64 and RV128;
    /// `None` means the encoding is not valid for that ISA.
    pub(crate) decompress: [Option<&'static Opcode>; 3],
    /// Operand values that make the encoding reserved: it is illegal if any
    /// of these hold.
    pub(crate) illegal_if: &'static [Constraint],
    /// Operand values that make the encoding a HINT, which objdump shows as
    /// encoded rather than expanded: it is a HINT if any of these hold.
    pub(crate) hint_if: &'static [Constraint],
    /// The base ISAs the instruction exists in, as a bit set indexed by
    /// [`Isa`].
    pub(crate) isas: u8,
}

impl Opcode {
    pub(crate) const fn new(name: &'static str, codec: Codec, format: &'static str) -> Self {
        Opcode {
            name,
            codec,
            format,
            pseudo: &[],
            decompress: [None; 3],
            illegal_if: &[],
            hint_if: &[],
            isas: 0b111,
        }
    }

    /// Marks an instruction as existing only in RV64 and RV128.
    pub(crate) const fn rv64(self) -> Self {
        Opcode {
            isas: 0b110,
            ..self
        }
    }

    /// Marks an instruction as existing only in RV32.
    pub(crate) const fn rv32_only(self) -> Self {
        Opcode {
            isas: 0b001,
            ..self
        }
    }

    /// Marks an instruction as existing only in RV64.
    pub(crate) const fn rv64_only(self) -> Self {
        Opcode {
            isas: 0b010,
            ..self
        }
    }

    /// Marks an instruction as existing only in RV128.
    pub(crate) const fn rv128(self) -> Self {
        Opcode {
            isas: 0b100,
            ..self
        }
    }

    pub(crate) fn exists_in(&self, isa: Isa) -> bool {
        match self.decompress {
            [None, None, None] => self.isas & (1 << isa as u8) != 0,
            expansions => expansions[isa as usize].is_some_and(|e| e.exists_in(isa)),
        }
    }
}

// Opcodes are compared by address, and `Debug` shows only the mnemonic
// rather than the alias and expansion tables.
impl PartialEq for Opcode {
    fn eq(&self, other: &Self) -> bool {
        core::ptr::eq(self, other)
    }
}

impl Eq for Opcode {}

impl core::fmt::Debug for Opcode {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.name)
    }
}

/// An encoding that is not a valid instruction, shown as objdump does.
pub(crate) static ILLEGAL: Opcode = Opcode::new(".insn", Codec::Illegal, fmt::INSN);
/// A 16-bit parcel of an encoding with a reserved length (10 bytes or more),
/// which `.insn` cannot express.
pub(crate) static RESERVED_PARCEL: Opcode = Opcode::new(".2byte", Codec::Illegal, fmt::RAW);