disc-v 0.1.0

A RISC-V disassembler
Documentation
#!/usr/bin/env python3
# Copyright (c) 2026 Christopher Swenson
# Licensed under the Apache-2.0 license.
"""Generates tests/data/riscv-opcodes.txt from a checkout of
https://github.com/riscv/riscv-opcodes.

Usage: scripts/gen-riscv-opcodes.py <riscv-opcodes checkout> > tests/data/riscv-opcodes.txt

Each output line describes one instruction encoding:

    <isas> <name> <base> <match> <mask> <nonzero> <not-two>

  isas     `32`, `64` or `32,64`: the base ISAs the encoding is valid for
  name     mnemonic, with riscv-opcodes' `.rv32`/`.rv64` suffixes removed
  base     for a pseudo-op, the instruction it is an alias of; otherwise `-`
  match    bits that must be set, in hex, under...
  mask     ...this mask
  nonzero  comma-separated field masks that must not be all zero, or `-`
           (for example `c.addi`'s rd and nonzero immediate)
  not-two  field mask that must not equal 2 (`c.lui`'s rd), or `-`
"""
import csv, os, re, subprocess, sys

# The extensions disc-v supports.
FILES = """rv_i rv32_i rv64_i rv_m rv64_m rv_a rv64_a rv_f rv64_f rv_d rv64_d
rv_q rv64_q rv_c rv32_c rv64_c rv_c_d rv32_c_f rv_zicsr rv_zifencei rv_system
rv_s rv_sdext rv_zicntr rv_zba rv64_zba rv_zbb rv32_zbb rv64_zbb rv_zbc rv_zbs
rv32_zbs rv64_zbs""".split()

root = sys.argv[1]
ext_dir = os.path.join(root, 'extensions')
fields = {name: (int(hi), int(lo)) for name, hi, lo in csv.reader(open(os.path.join(root, 'arg_lut.csv')), skipinitialspace=True)}

def field_mask(name):
    hi, lo = fields[name]
    return ((1 << (hi - lo + 1)) - 1) << lo

def parse_line(tokens):
    """Returns (name, match, mask, nonzero masks, not-two mask)."""
    name, rest = tokens[0], tokens[1:]
    match = mask = 0
    nz, n2 = {}, 0
    for t in rest:
        m = re.fullmatch(r'(\d+)(?:\.\.(\d+))?=(\w+)', t)
        if m:
            hi = int(m.group(1)); lo = int(m.group(2) or hi)
            bits = ((1 << (hi - lo + 1)) - 1) << lo
            match |= (int(m.group(3), 0) << lo) & bits
            mask |= bits
            continue
        if t.endswith('_n0') or t.endswith('_n0_e') or t in ('rd_n2',):
            nz[t] = field_mask(t)
        if t == 'rd_n2':
            n2 = field_mask(t)
        nm = re.match(r'c_nz(u?imm\d+)', t)
        if nm:  # a nonzero immediate split across several fields
            nz[nm.group(1)] = nz.get(nm.group(1), 0) | field_mask(t)
    return name, match, mask, list(nz.values()), n2

def file_lines(f):
    for line in open(os.path.join(ext_dir, f)):
        line = line.split('#', 1)[0].strip()
        if line:
            yield line.split()

def find_def(f, name):
    for tokens in file_lines(f):
        if tokens[0] == name:
            return tokens
    raise KeyError(f'{name} not in {f}')

def strip_suffix(name):
    return re.sub(r'[._]rv(32|64)$', '', name)

rev = subprocess.run(['git', '-C', root, 'rev-parse', 'HEAD'], capture_output=True, text=True).stdout.strip()
print(f'# Generated by scripts/gen-riscv-opcodes.py from riscv-opcodes {rev}.')
print('# riscv-opcodes is Copyright (c) 2022 RISC-V International, BSD-3-Clause.')
print('# isas name base match mask nonzero not-two')
for f in FILES:
    isas = {'rv': '32,64', 'rv32': '32', 'rv64': '64'}[f.split('_')[0]]
    for tokens in file_lines(f):
        base = '-'
        if tokens[0] == '$import':
            src, name = tokens[1].split('::')
            tokens = find_def(src, name)
        elif tokens[0] == '$pseudo_op':
            base = strip_suffix(tokens[1].split('::')[1])
            tokens = tokens[2:]
        name, match, mask, nz, n2 = parse_line(tokens)
        nzs = ','.join(f'{m:x}' for m in nz) or '-'
        print(f"{isas} {strip_suffix(name)} {base} {match:x} {mask:x} {nzs} {n2:x}" if n2 else
              f"{isas} {strip_suffix(name)} {base} {match:x} {mask:x} {nzs} -")