# AMD ISA code generator - generates enum.py, ins.py, operands.py, str_pcode.py
# Sources: XML from https://gpuopen.com/download/machine-readable-isa/latest/
#          PDF manuals from AMD documentation
import re, zlib, xml.etree.ElementTree as ET, zipfile, pathlib
from tinygrad.helpers import fetch

# ═══════════════════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════════════════

ARCHS = {
  "rdna3": {"xml": "amdgpu_isa_rdna3_5.xml", "pdf": "https://gpuopen.com/download/rdna35_instruction_set_architecture.pdf"},
  "rdna4": {"xml": "amdgpu_isa_rdna4.xml", "pdf": "https://gpuopen.com/download/rdna4-instruction-set-architecture.pdf"},
  "cdna": {"xml": "amdgpu_isa_cdna4.xml", "pdf": "https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/instruction-set-architectures/amd-instinct-cdna4-instruction-set-architecture.pdf"},
}
# Pin the September 2025 XML bundle because newer `latest` changed WMMA format bit sizes across archs and breaks generation.
XML_URL = "https://gpuopen.com/download/AMD_GPU_MR_ISA_XML_2025_09_05.zip"
# Map XML encoding names to codebase names
NAME_MAP = {"VOP3_SDST_ENC": "VOP3SD", "VOP3_SDST_ENC_LIT": "VOP3SD_LIT", "VOP3_SDST_ENC_DPP16": "VOP3SD_DPP16",
            "VOP3_SDST_ENC_DPP8": "VOP3SD_DPP8", "VOPDXY": "VOPD", "VOPDXY_LIT": "VOPD_LIT", "VDS": "DS"}
# Instructions missing from XML but present in PDF
FIXES = {"rdna3": {"SOPK": {22: "S_SUBVECTOR_LOOP_BEGIN", 23: "S_SUBVECTOR_LOOP_END"}, "FLAT": {55: "FLAT_ATOMIC_CSUB_U32"}},
         "rdna4": {"SOP1": {80: "S_GET_BARRIER_STATE", 81: "S_BARRIER_INIT", 82: "S_BARRIER_JOIN"}, "SOPP": {9: "S_WAITCNT", 21: "S_BARRIER_LEAVE"}},
         "cdna": {"DS": {152: "DS_GWS_SEMA_RELEASE_ALL", 154: "DS_GWS_SEMA_V", 156: "DS_GWS_SEMA_P"},
                  "VOP3P": {44: "V_MFMA_LD_SCALE_B32", 62: "V_MFMA_F32_16X16X8_XF32", 63: "V_MFMA_F32_32X32X4_XF32"}}}
# Fields missing from XML but present in hardware (format: {arch: {encoding: [(name, hi, lo), ...]}})
FIELD_FIXES = {"cdna": {"VOP3P": [("opsel_hi2", 14, 14)]}}
# Fields variable in XML but fixed in hardware (format: {arch: {encoding: {name: value or (encoding, opcode) of the embedded instruction}}})
FIXED_FIELDS: dict[str, dict[str, dict[str, int|tuple[str, str]]]] = {
  "cdna": {"VOP3PX2": {"x2encoding": ("VOP3P", "V_MFMA_LD_SCALE_B32"), "abid": 1}}}
# Encoding suffixes to strip (variants we don't generate separate classes for)
_ENC_SUFFIXES = ("_NSA1",)
# Encoding suffix to class suffix mapping (for variants we DO generate)
_ENC_SUFFIX_MAP = {"_INST_LITERAL": "_LIT", "_VOP_DPP16": "_DPP16", "_VOP_DPP": "_DPP16", "_VOP_DPP8": "_DPP8",
                   "_VOP_SDWA": "_SDWA", "_VOP_SDWA_SDST_ENC": "_SDWA_SDST", "_MFMA": "_MFMA"}
# Field name normalization
_FIELD_RENAMES = {"opsel_hi_2": "opsel_hi2", "op_sel_hi_2": "opsel_hi2", "op_sel": "opsel", "bound_ctrl": "bc",
                  "tgt": "target", "row_en": "row", "unorm": "unrm", "clamp": "clmp", "wait_exp": "waitexp",
                  "simm32": "literal", "dpp_ctrl": "dpp", "acc_cd": "acc_cd", "acc": "acc",
                  "dst_sel": "dst_sel", "dst_unused": "dst_unused", "src0_sel": "src0_sel", "src1_sel": "src1_sel"}
# Encoding variants to skip entirely (NSA is for MIMG graphics instructions)
_SKIP_ENCODINGS = ("NSA",)

# ═══════════════════════════════════════════════════════════════════════════════
# XML parsing helpers
# ═══════════════════════════════════════════════════════════════════════════════

def _strip_enc(name: str) -> str:
  """Strip ENC_ prefix and normalize encoding suffixes."""
  name = name.removeprefix("ENC_")
  for sfx in _ENC_SUFFIXES: name = name.replace(sfx, "")
  # Process longer suffixes first to avoid partial matches (e.g., _VOP_DPP8 before _VOP_DPP)
  for old, new in sorted(_ENC_SUFFIX_MAP.items(), key=lambda x: -len(x[0])): name = name.replace(old, new)
  return name

def _norm_field(name: str) -> str:
  """Normalize field name to match expected names."""
  for old, new in _FIELD_RENAMES.items(): name = name.replace(old, new)
  return name

def _map_flat(enc_name: str, instr_name: str) -> str:
  """Map FLAT/GLOBAL/SCRATCH encoding to correct enum based on instruction prefix."""
  if enc_name in ("FLAT_GLBL", "FLAT_GLOBAL"): return "GLOBAL"
  if enc_name == "FLAT_SCRATCH": return "SCRATCH"
  if enc_name in ("FLAT", "VFLAT", "VGLOBAL", "VSCRATCH"):
    v = "V" if enc_name.startswith("V") else ""
    if instr_name.startswith("GLOBAL_"): return f"{v}GLOBAL"
    if instr_name.startswith("SCRATCH_"): return f"{v}SCRATCH"
    return f"{v}FLAT"
  return enc_name

# ═══════════════════════════════════════════════════════════════════════════════
# XML parsing
# ═══════════════════════════════════════════════════════════════════════════════

def parse_xml(filename: str):
  root = ET.fromstring(zipfile.ZipFile(fetch(XML_URL)).read(filename))
  encodings, enums, types, fmts, op_types_set = {}, {}, {}, {}, set()
  # Extract HWREG and MSG enums from OperandTypes
  op_enum_map = {("OPR_HWREG", "ID"): "HWREG", ("OPR_SENDMSG_RTN", "MSG"): "MSG"}
  for ot in root.findall(".//OperandTypes/OperandType"):
    ot_name = ot.findtext("OperandTypeName")
    for field in ot.findall(".//Field"):
      key = (ot_name, field.findtext("FieldName"))
      if (enum_name := op_enum_map.get(key)):  # type: ignore[arg-type]
        def _pv_val(pv: ET.Element) -> tuple[int, str]:
          v, n = pv.findtext("Value"), pv.findtext("Name")
          assert v is not None and n is not None
          return int(v), n.upper()
        enums[enum_name] = dict(_pv_val(pv) for pv in field.findall(".//PredefinedValue"))
  # Extract DataFormats with BitCount
  for df in root.findall("ISA/DataFormats/DataFormat"):
    name, bits = df.findtext("DataFormatName"), df.findtext("BitCount")
    if name and bits: fmts[name] = int(bits)
  # Extract encoding definitions
  for enc in root.findall("ISA/Encodings/Encoding"):
    name = enc.findtext("EncodingName")
    assert name is not None
    is_base = name.startswith("ENC_") or name in ("VOP3_SDST_ENC", "VOPDXY")
    is_variant = any(sfx in name for sfx in _ENC_SUFFIX_MAP)
    if not is_base and not is_variant: continue
    if any(s in name for s in _SKIP_ENCODINGS): continue
    fields: list[tuple[str, int, int]] = []
    for f in enc.findall(".//MicrocodeFormat/BitMap/Field"):
      br = f.find("BitLayout/Range")
      if br is None: continue
      fn = f.findtext("FieldName")
      assert fn is not None
      fields.append((_norm_field(fn.lower()),
        int(br.findtext("BitOffset") or 0) + int(br.findtext("BitCount") or 0) - 1, int(br.findtext("BitOffset") or 0)))
    ident_list = enc.findall("EncodingIdentifiers/EncodingIdentifier")
    ident = ident_list[0] if ident_list else None
    enc_field = next((f for f in fields if f[0] == "encoding"), None)
    # For multi-dword formats, encoding field may be in higher dword but identifier is always in dword0; use % 32
    enc_bits: str | None = None
    if ident is not None and ident.text is not None and enc_field:
      enc_bits = "".join(ident.text[len(ident.text)-1-b] for b in range(enc_field[1] % 32, (enc_field[2] % 32)-1, -1))
    base_name = _strip_enc(name)
    encodings[NAME_MAP.get(base_name, base_name)] = (fields, enc_bits)
  # Extract instruction opcodes and operand info
  # Track which encodings each opcode appears in (for detecting LIT-only ops)
  opcode_encs: dict[str, dict[int, set[str]]] = {}  # {base_fmt: {opcode: {enc_names}}}
  for instr in root.findall("ISA/Instructions/Instruction"):
    name = instr.findtext("InstructionName")
    assert name is not None
    for enc in instr.findall("InstructionEncodings/InstructionEncoding"):
      if enc.findtext("EncodingCondition") != "default": continue
      enc_enc_name = enc.findtext("EncodingName")
      assert enc_enc_name is not None
      base, opcode = _map_flat(_strip_enc(enc_enc_name), name), int(enc.findtext("Opcode") or 0)
      enc_name = NAME_MAP.get(base, base)
      # Encoding variants use the same Op enum as the base format
      base_enum = enc_name
      for sfx in ("_SDWA_SDST", "_DPP16", "_DPP8", "_SDWA", "_LIT", "_MFMA"):
        base_enum = base_enum.replace(sfx, "")
      # Track which encodings this opcode appears in
      opcode_encs.setdefault(base_enum, {}).setdefault(opcode, set()).add(enc_name)
      # ADDTID instructions go in both FLAT and GLOBAL enums (pcode uses FLATOp for these)
      if "ADDTID" in name:
        if base == "GLOBAL": enums.setdefault("FLAT", {})[opcode] = name
        elif base == "VGLOBAL": enums.setdefault("VFLAT", {})[opcode] = name
      enums.setdefault(base_enum, {})[opcode] = name
      # Extract operand info
      op_info: dict[str, tuple[str | None, int, str | None]] = {}
      for op in enc.findall("Operands/Operand"):
        fn = op.findtext("FieldName")
        if fn: op_info[fn.lower()] = (op.findtext("DataFormatName"), int(op.findtext("OperandSize") or 0), op.findtext("OperandType"))
      for fmt, _, otype in op_info.values():
        if fmt and fmt not in fmts: fmts[fmt] = 0
        if otype: op_types_set.add(otype)
      if op_info: types[(name, base_enum)] = op_info
  # Find opcodes that only exist in a specific variant encoding (no base format version)
  suffix_only_ops: dict[str, dict[str, set[int]]] = {}  # {suffix: {base_fmt: {opcodes}}}
  for base_fmt, opcodes in opcode_encs.items():
    for opcode, encs in opcodes.items():
      suffix = next((s for s in _ENC_SUFFIX_MAP.values() if all(s in e for e in encs)), None)
      if suffix is not None: suffix_only_ops.setdefault(suffix, {}).setdefault(base_fmt, set()).add(opcode)
  return encodings, enums, types, fmts, op_types_set, suffix_only_ops

# ═══════════════════════════════════════════════════════════════════════════════
# PDF parsing
# ═══════════════════════════════════════════════════════════════════════════════

def extract_pdf_text(url: str) -> list[list[tuple[float, float, str, str]]]:
  """Extract positioned text from PDF. Returns list of text elements (x, y, text, font) per page."""
  data = fetch(url).read_bytes()
  # Parse xref table to locate objects
  xref: dict[int, int] = {}
  xref_match = re.search(rb'startxref\s+(\d+)', data)
  assert xref_match is not None
  pos = int(xref_match.group(1)) + 4
  while data[pos:pos+7] != b'trailer':
    while data[pos:pos+1] in b' \r\n': pos += 1
    line_end = data.find(b'\n', pos)
    start_obj, count = map(int, data[pos:line_end].split()[:2])
    pos = line_end + 1
    for i in range(count):
      if data[pos+17:pos+18] == b'n' and (off := int(data[pos:pos+10])) > 0: xref[start_obj + i] = off
      pos += 20

  def get_stream(n: int) -> bytes:
    obj = data[xref[n]:data.find(b'endobj', xref[n])]
    raw = obj[obj.find(b'stream\n') + 7:obj.find(b'\nendstream')]
    return zlib.decompress(raw) if b'/FlateDecode' in obj else raw

  pages = []
  for n in sorted(xref):
    if b'/Type /Page' not in data[xref[n]:xref[n]+500]: continue
    if not (m := re.search(rb'/Contents (\d+) 0 R', data[xref[n]:xref[n]+500])): continue
    stream = get_stream(int(m.group(1))).decode('latin-1')
    elements, font = [], ''
    _RE_BT = (r'(/F[\d.]+) [\d.]+ Tf|([\d.+-]+) ([\d.+-]+) Td|[\d.+-]+ [\d.+-]+ [\d.+-]+ [\d.+-]+ ([\d.+-]+) ([\d.+-]+) Tm'
              r'|<([0-9A-Fa-f]+)>.*?Tj|\[([^\]]+)\] TJ')
    for bt in re.finditer(r'BT(.*?)ET', stream, re.S):
      x, y = 0.0, 0.0
      for sm in re.finditer(_RE_BT, bt.group(1)):
        if sm.group(1): font = sm.group(1)
        elif sm.group(2): x, y = x + float(sm.group(2)), y + float(sm.group(3))
        elif sm.group(4): x, y = float(sm.group(4)), float(sm.group(5))
        elif sm.group(6) and (t := bytes.fromhex(sm.group(6)).decode('latin-1')).strip():
          elements.append((x, y, t, font))
        elif sm.group(7):
          t = ''.join(bytes.fromhex(h).decode('latin-1') for h in re.findall(r'<([0-9A-Fa-f]+)>', sm.group(7)))
          if t.strip(): elements.append((x, y, t, font))
    pages.append(sorted(elements, key=lambda e: (-e[1], e[0])))
  return pages

def extract_pcode(pages: list[list[tuple[float, float, str, str]]], name_to_op: dict[str, int]) -> dict[tuple[str, int], str]:
  """Extract pseudocode for instructions. Returns {(name, opcode): pseudocode}."""
  # First pass: find all instruction headers across all pages
  all_instructions: list[tuple[int, float, str, int]] = []  # (page_idx, y, name, opcode)
  for page_idx, page in enumerate(pages):
    by_y: dict[int, list[tuple[float, str]]] = {}
    for x, y, t, _ in page:
      by_y.setdefault(round(y), []).append((x, t))
    for y, items in sorted(by_y.items(), reverse=True):
      left = [(x, t) for x, t in items if 55 < x < 65]
      right = [(x, t) for x, t in items if 535 < x < 550]
      if left and right and left[0][1] in name_to_op and right[0][1].isdigit():
        all_instructions.append((page_idx, y, left[0][1], int(right[0][1])))

  # Second pass: extract pseudocode between consecutive instructions
  pcode: dict[tuple[str, int], str] = {}
  for i, (page_idx, y, name, opcode) in enumerate(all_instructions):
    if i + 1 < len(all_instructions):
      next_page, next_y = all_instructions[i + 1][0], all_instructions[i + 1][1]
    else:
      next_page, next_y = page_idx, 0
    # Collect F6 text from current position to next instruction (pseudocode is at x ≈ 69)
    lines: list[tuple[int, float, str]] = []
    for p in range(page_idx, next_page + 1):
      start_y = y if p == page_idx else 800
      end_y = next_y if p == next_page else 0
      lines.extend((p, y2, t) for x, y2, t, f in pages[p] if f in ('/F6.0', '/F7.0') and end_y < y2 < start_y and 60 < x < 80)
    if lines:
      sorted_lines = sorted(lines, key=lambda x: (x[0], -x[1]))
      # Stop at large Y gaps (>30) - indicates section break
      filtered = [sorted_lines[0]]
      depth = sorted_lines[0][2].count("{") - sorted_lines[0][2].count("}")
      for j in range(1, len(sorted_lines)):
        prev_page, prev_y, _ = sorted_lines[j-1]
        curr_page, curr_y, _ = sorted_lines[j]
        if depth == 0 and curr_page == prev_page and prev_y - curr_y > 30: break
        if depth == 0 and curr_page != prev_page and prev_y > 60 and curr_y < 730: break
        filtered.append(sorted_lines[j])
        code = sorted_lines[j][2].split("//")[0]
        depth += code.count("{") - code.count("}")
      pcode_lines = [t.replace('Ê', '').strip() for _, _, t in filtered]
      if pcode_lines: pcode[(name, opcode)] = '\n'.join(pcode_lines)
  return pcode

# ═══════════════════════════════════════════════════════════════════════════════
# Code generation
# ═══════════════════════════════════════════════════════════════════════════════

def write_common(all_fmts: dict[str, int], all_op_types: set[str], path: pathlib.Path) -> None:
  lines: list[str] = ["# autogenerated from AMD ISA XML - do not edit", "from enum import Enum, auto", ""]
  lines.append("class ReprEnum(Enum):")
  lines.append('  """Enum with clean repr that roundtrips with eval()."""')
  lines.append('  def __repr__(self): return f"{type(self).__name__}.{self.name}"')
  lines.append("")
  lines.append("class Fmt(Enum):")
  for fmt in sorted(all_fmts.keys()): lines.append(f"  {fmt} = auto()")
  lines.append("")
  lines.append("FMT_BITS = {")
  for fmt, bits in sorted(all_fmts.items()): lines.append(f"  Fmt.{fmt}: {bits},")
  lines.append("}")
  lines.append("")
  lines.append("class OpType(Enum):")
  for ot in sorted(all_op_types): lines.append(f"  {ot} = auto()")
  with open(path, "w") as f: f.write("\n".join(lines))

def write_enum(enums, path):
  lines: list[str] = ["# autogenerated from AMD ISA XML - do not edit",
    "from tinygrad.runtime.autogen.amd.common import ReprEnum, Fmt, FMT_BITS, OpType  # noqa: F401", ""]
  for name, ops in sorted(enums.items()):
    if not ops: continue
    suffix = "_E32" if name in ("VOP1", "VOP2", "VOPC") else "_E64" if name == "VOP3" else ""
    lines.append(f"class {name}(ReprEnum):" if name in ("HWREG", "MSG") else f"class {name}Op(ReprEnum):")
    aliases = []
    for op, mem in sorted(ops.items()):
      msuf = suffix if name != "VOP3" or op < 512 else ""
      lines.append(f"  {mem}{msuf} = {op}")
      if msuf: aliases.append((mem, msuf))
    for mem, msuf in aliases: lines.append(f"  {mem} = {mem}{msuf}")
    lines.append("")
  with open(path, "w") as f: f.write("\n".join(lines))

def write_ins(encodings, enums, suffix_only_ops, types, arch, path):
  _VGPR_FIELDS = {"vdst", "vdstx", "vsrc0", "vsrc1", "vsrc2", "vsrc3", "vsrcx1", "vsrcy1", "vaddr", "vdata", "data", "data0", "data1", "addr", "vsrc"}
  _VARIANT_SUFFIXES = ("_LIT", "_DPP16", "_DPP8", "_SDWA_SDST", "_SDWA", "_MFMA")
  def get_base_fmt(fmt):
    for sfx in _VARIANT_SUFFIXES: fmt = fmt.replace(sfx, "")
    return fmt
  def field_def(name, hi, lo, fmt, enc_bits=None):
    bits = hi - lo + 1
    base_fmt = get_base_fmt(fmt)
    if name == "encoding" and enc_bits: return f"FixedBitField({hi}, {lo}, 0b{enc_bits})"
    if (fixed := FIXED_FIELDS.get(arch, {}).get(fmt, {}).get(name)) is not None:
      if isinstance(fixed, int): return f"FixedBitField({hi}, {lo}, {fixed})"
      fixed_bits, opcode = encodings[fixed[0]][1], next(op for op, n in enums[fixed[0]].items() if n == fixed[1])
      return f"FixedBitField({hi}, {lo}, 0b{fixed_bits}{opcode:0{bits-len(fixed_bits)}b})"
    if name == "op" and fmt not in ("DPP", "SDWA"): return f"EnumBitField({hi}, {lo}, {base_fmt}Op)"
    if name in ("opx", "opy"): return f"EnumBitField({hi}, {lo}, VOPDOp)"
    if name == "vdsty": return f"VDSTYField({hi}, {lo})"
    if name in _VGPR_FIELDS and bits == 8: return f"VGPRField({hi}, {lo})"
    if name == "sbase" and bits == 6: return f"SBaseField({hi}, {lo})"
    if name in ("srsrc", "ssamp") and bits == 5: return f"SRsrcField({hi}, {lo})"
    if name in ("sdst", "sdata") and bits == 7: return f"SGPRField({hi}, {lo})"
    if name in ("soffset", "saddr") and bits == 7: return f"SGPRField({hi}, {lo}, default=NULL)"
    if name.startswith("ssrc") and bits == 8: return f"SSrcField({hi}, {lo})"
    if name in ("saddr", "soffset") and bits == 8: return f"SSrcField({hi}, {lo}, default=NULL)"
    if name.startswith("src") and bits == 9: return f"SrcField({hi}, {lo})"
    # GLOBAL/SCRATCH: offset is 13-bit signed [12:0], FLAT: 12-bit unsigned (XML has 12-bit for all)
    if name == "offset" and base_fmt in ("GLOBAL", "SCRATCH"): return f"BitField(12, {lo})"
    if base_fmt == "VOP3P" and name == "opsel_hi": return f"BitField({hi}, {lo}, default=3)"
    if base_fmt == "VOP3P" and name == "opsel_hi2": return f"BitField({hi}, {lo}, default=1)"
    return f"BitField({hi}, {lo})"
  ORDER = ['encoding', 'op', 'opx', 'opy', 'vdst', 'vdstx', 'vdsty', 'sdst', 'vdata', 'sdata', 'addr', 'vaddr', 'data', 'data0', 'data1',
           'src0', 'srcx0', 'srcy0', 'vsrc0', 'ssrc0', 'src1', 'vsrc1', 'vsrcx1', 'vsrcy1', 'ssrc1', 'src2', 'vsrc2', 'src3', 'vsrc3',
           'saddr', 'sbase', 'srsrc', 'ssamp', 'soffset', 'offset', 'simm16', 'literal', 'en', 'target', 'attr', 'attr_chan',
           'omod', 'neg', 'neg_hi', 'abs', 'clmp', 'opsel', 'opsel_hi', 'waitexp', 'wait_va',
           'dmask', 'dim', 'seg', 'format', 'offen', 'idxen', 'glc', 'dlc', 'slc', 'tfe', 'unrm', 'done', 'row',
           'dpp', 'fi', 'bc', 'row_mask', 'bank_mask', 'src0_neg', 'src0_abs', 'src1_neg', 'src1_abs',
           'cbsz', 'abid', 'acc_cd', 'acc', 'blgp', 'lane_sel_0', 'lane_sel_1', 'lane_sel_2', 'lane_sel_3',
           'lane_sel_4', 'lane_sel_5', 'lane_sel_6', 'lane_sel_7', 'dst_sel', 'dst_unused', 'src0_sel', 'src1_sel']
  def sort_fields(fields): return sorted(fields, key=lambda f: (ORDER.index(f[0]) if f[0] in ORDER else 999, f[2]))

  # Separate base encodings from variants
  base_encodings, variant_encodings = {}, {}
  for enc_name, data in encodings.items():
    base = get_base_fmt(enc_name)
    if base == enc_name: base_encodings[enc_name] = data
    else: variant_encodings[enc_name] = data

  # Build sets of ops by their vdst type from operand metadata
  sdst_opcodes: dict[str, set[int]] = {}  # ops where vdst is OPR_SREG (writes to SGPR)
  for fmt, ops in enums.items():
    for op, name in ops.items():
      op_types = types.get((name, fmt), {})
      vdst_type = op_types.get("vdst", (None, None, None))[2]
      if vdst_type == "OPR_SREG": sdst_opcodes.setdefault(fmt, set()).add(op)

  # collect only the XxxOp enums that are actually referenced in this arch's instruction definitions
  enum_names = sorted(f"{k}Op" for k in enums if enums[k] and k not in ("HWREG", "MSG"))
  # also re-export HWREG/MSG enums (plain enums, not instruction format ops)
  enum_names += sorted(k for k in enums if k in ("HWREG", "MSG") and enums[k])
  # collect DSL field types actually used by scanning generated field definitions
  all_field_defs = " ".join(field_def(fn, hi, lo, enc, eb) for enc, (flds, eb) in encodings.items() for fn, hi, lo in flds)
  _ALL_DSL = ["BitField", "EnumBitField", "FixedBitField", "NULL", "SBaseField", "SGPRField", "SRsrcField",
              "SSrcField", "SrcField", "VDSTYField", "VGPRField"]
  dsl_names = ["Inst"] + [n for n in _ALL_DSL if n in all_field_defs]
  # also re-export register names so `from ins import *` still provides them to downstream users
  _DSL_REGS = ["s", "v", "src", "VCC_LO", "VCC_HI", "VCC", "EXEC_LO", "EXEC_HI", "EXEC", "NULL", "OFF", "M0",
               "SCC", "VCCZ", "EXECZ", "ttmp", "INV_2PI", "SDWA", "DPP", "DPP16", "LIT", "SRC_LDS_DIRECT"]
  dsl_reexport = sorted(set(dsl_names + _DSL_REGS))
  lines: list[str] = ["# autogenerated from AMD ISA XML - do not edit", "# ruff: noqa: E501,F401",
    f"from tinygrad.renderer.amd.dsl import {', '.join(dsl_reexport)}",
    f"from tinygrad.runtime.autogen.amd.{arch}.enum import {', '.join(enum_names)}", "import functools", ""]

  def fmt_allowed(op_enum: str, ops: set[int]) -> str:
    """Format allowed ops as {EnumName.MEMBER, ...}."""
    names = [f"{op_enum}.{enums[op_enum.removesuffix('Op')][op]}" for op in sorted(ops)]
    return "{" + ", ".join(names) + "}"

  # Generate base classes first
  for enc_name, (fields, enc_bits) in sorted(base_encodings.items()):
    all_ops = set(enums.get(enc_name, {}).keys())
    # Get suffix-only ops for this format (these can't be used in base class)
    base_suffix_ops = set().union(*(d.get(enc_name, set()) for d in suffix_only_ops.values()))
    # Exclude SDST ops from base class (they need VOP1_SDST/VOP3_SDST/VOP3B)
    base_allowed = all_ops - base_suffix_ops - sdst_opcodes.get(enc_name, set())
    # RDNA3 FLAT/GLOBAL/SCRATCH share encoding bits, differentiated by seg field
    # RDNA4 VFLAT/VGLOBAL/VSCRATCH have distinct encoding bits, no seg field needed
    has_seg_field = any(fn == "seg" for fn, _, _ in fields)
    if enc_name in ("FLAT", "VFLAT") and has_seg_field:
      prefix = "V" if enc_name == "VFLAT" else ""
      flat_variants = [(f"{prefix}FLAT", 0, f"{prefix}FLATOp"), (f"{prefix}GLOBAL", 2, f"{prefix}GLOBALOp"),
                       (f"{prefix}SCRATCH", 1, f"{prefix}SCRATCHOp")]
      for cls, seg, op_enum in flat_variants:
        cls_ops = set(enums.get(cls, {}).keys())
        lines.append(f"class {cls}(Inst):")
        for fn, hi, lo in sort_fields(fields):
          if fn == "seg": lines.append(f"  seg = FixedBitField({hi}, {lo}, {seg})")
          elif fn == "op": lines.append(f"  op = EnumBitField({hi}, {lo}, {op_enum}, {fmt_allowed(op_enum, cls_ops)})")
          else: lines.append(f"  {fn} = {field_def(fn, hi, lo, cls, enc_bits)}")
        lines.append("")
    elif enc_name not in ("FLAT_GLOBAL", "FLAT_SCRATCH", "FLAT_GLBL", "DPP", "SDWA"):
      lines.append(f"class {enc_name}(Inst):")
      for fn, hi, lo in sort_fields(fields):
        if fn == "op":
          base_fmt = get_base_fmt(enc_name)
          lines.append(f"  op = EnumBitField({hi}, {lo}, {base_fmt}Op, {fmt_allowed(f'{base_fmt}Op', base_allowed)})")
        else:
          lines.append(f"  {fn} = {field_def(fn, hi, lo, enc_name, enc_bits if fn == 'encoding' else None)}")
      lines.append("")

  # Generate variant classes that inherit from base (only add extra fields)
  for enc_name, (fields, enc_bits) in sorted(variant_encodings.items()):
    base = get_base_fmt(enc_name)
    if base not in base_encodings: continue  # skip if no base class
    base_fields = {f[0] for f in base_encodings[base][0]}
    extra_fields = [(fn, hi, lo) for fn, hi, lo in fields if fn not in base_fields]
    # Check if this is a suffix-only variant
    variant_suffix = next((sfx for sfx in _VARIANT_SUFFIXES if enc_name.endswith(sfx)), None)
    is_suffix_variant = variant_suffix in suffix_only_ops
    all_ops = set(enums.get(base, {}).keys())
    if extra_fields or is_suffix_variant:
      lines.append(f"class {enc_name}({base}):")
      op_field = next((f for f in base_encodings[base][0] if f[0] == "op"), None)
      # _LIT classes: override op to allow all opcodes (base excludes lit-only ops)
      # other classes override op to only suffix-only opcodes
      if op_field and is_suffix_variant:
        _, hi, lo = op_field
        allowed_ops = all_ops if variant_suffix == "_LIT" else suffix_only_ops[variant_suffix][base]
        lines.append(f"  op = EnumBitField({hi}, {lo}, {base}Op, {fmt_allowed(f'{base}Op', allowed_ops)})")
      for fn, hi, lo in sort_fields(extra_fields):
        lines.append(f"  {fn} = {field_def(fn, hi, lo, enc_name)}")
      lines.append("")

  # SDST variants (special case - redefine vdst field type, restrict to SDST ops)
  for base, field_hi, field_lo in [("VOP1", 24, 17), ("VOP3", 7, 0)]:
    if base not in base_encodings: continue
    sdst_ops = sdst_opcodes.get(base, set())
    if not sdst_ops: continue
    # For VOP3, all ops < 256 (compare/cmpx ops) use SDST encoding
    all_base_ops = set(enums.get(base, {}).keys())
    if base == "VOP3": sdst_ops = sdst_ops | {op for op in all_base_ops if op < 256}
    op_field = next((f for f in base_encodings[base][0] if f[0] == "op"), None)
    lines.append(f"class {base}_SDST({base}):")
    if op_field:
      _, hi, lo = op_field
      lines.append(f"  op = EnumBitField({hi}, {lo}, {base}Op, {fmt_allowed(f'{base}Op', sdst_ops)})")
    lines.append(f"  vdst = SSrcField({field_hi}, {field_lo})")
    lines.append("")
    # SDST_LIT class (for literals with SDST destination) - same ops, just adds literal field
    lit_enc = variant_encodings.get(f"{base}_LIT")
    if lit_enc:
      lit_field = next((f for f in lit_enc[0] if f[0] == "literal"), None)
      if lit_field:
        lines.append(f"class {base}_SDST_LIT({base}_SDST):")
        lines.append(f"  literal = BitField({lit_field[1]}, {lit_field[2]})")
        lines.append("")

  # Instruction helpers
  lines.append("# instruction helpers")
  for fmt, ops in sorted(enums.items()):
    if fmt not in base_encodings and fmt not in ("GLOBAL", "SCRATCH", "VGLOBAL", "VSCRATCH"): continue
    suffix = "_E32" if fmt in ("VOP1", "VOP2", "VOPC") else "_E64" if fmt == "VOP3" else ""
    op_to_suffix = {op:suffix for suffix,ops in suffix_only_ops.items() for op in ops.get(fmt, set())}
    fmt_sdst_ops = sdst_opcodes.get(fmt, set())
    for op, name in sorted(ops.items()):
      # ADDTID ops are in both FLAT and GLOBAL enums (for pcode); only generate helper for GLOBAL/VGLOBAL
      if "ADDTID" in name and fmt in ("FLAT", "VFLAT"): continue
      msuf = suffix if fmt != "VOP3" or op < 512 else ""
      # Determine class: SDST variants, suffix-specific variants (e.g., _MFMA, _LIT), or base
      if fmt == "VOP1" and op in fmt_sdst_ops: cls = "VOP1_SDST"
      elif fmt == "VOP3" and (op in fmt_sdst_ops or op < 256): cls = "VOP3_SDST"
      elif op_to_suffix.get(op): cls = f"{fmt}{op_to_suffix[op]}"
      else: cls = fmt
      lines.append(f"{name.lower()}{msuf.lower()} = functools.partial({cls}, {fmt}Op.{name}{msuf})")
  with open(path, "w") as f: f.write("\n".join(lines))

def write_operands(types: dict, enums: dict, arch: str, path: pathlib.Path) -> None:
  valid = {(name, fmt) for fmt, ops in enums.items() for name in ops.values()}
  # only import enums that are actually used as keys in OPERANDS
  used_bases = {eb for (nm, eb) in types if (nm, eb) in valid}
  enum_names = sorted(f"{k}Op" for k in used_bases)
  lines: list[str] = ["# autogenerated from AMD ISA XML - do not edit",
    "from tinygrad.runtime.autogen.amd.common import Fmt, OpType",
    f"from tinygrad.runtime.autogen.amd.{arch}.enum import {', '.join(enum_names)}", ""]
  lines.append("# instruction operand info: {Op: {field: (Fmt, size_bits, OpType)}}")
  lines.append("OPERANDS = {")
  def fmt_val(v):
    fmt, size, otype = v
    return f"({f'Fmt.{fmt}' if fmt else 'None'}, {size}, {f'OpType.{otype}' if otype else 'None'})"
  for (name, enc_base), fields in sorted(types.items()):
    if (name, enc_base) not in valid: continue
    fstr = ", ".join(f'"{k}": {fmt_val(v)}' for k, v in sorted(fields.items()))
    lines.append(f'  {enc_base}Op.{name}: {{{fstr}}},')
  lines.append("}")
  with open(path, "w") as f: f.write("\n".join(lines))

def write_pcode(pcode: dict[tuple[str, int], str], enums: dict[str, dict[int, str]], arch: str, path: pathlib.Path) -> None:
  """Write str_pcode.py file from extracted pseudocode."""
  entries: list[tuple[str, str, int, str]] = []
  for fmt_name, ops in enums.items():
    member_suffix = "_E32" if fmt_name in ("VOP1", "VOP2", "VOPC") else "_E64" if fmt_name == "VOP3" else ""
    for opcode, name in ops.items():
      if (name, opcode) in pcode:
        msuf = member_suffix if fmt_name != "VOP3" or opcode < 512 else ""
        entries.append((f"{fmt_name}Op", f"{name}{msuf}", opcode, pcode[(name, opcode)]))
  enum_names = sorted(set(e[0] for e in entries))
  lines = ["# autogenerated from AMD ISA PDF - do not edit", "# ruff: noqa: E501",
           f"from tinygrad.runtime.autogen.amd.{arch}.enum import {', '.join(enum_names)}", "", "PCODE = {"]
  for enum_name, name, opcode, code in sorted(entries, key=lambda x: (x[0], x[2])):
    lines.append(f"  {enum_name}.{name}: {code!r},")
  lines.append("}")
  with open(path, "w") as f: f.write("\n".join(lines))

# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════

if __name__ == "__main__":
  all_fmts: dict[str, int] = {}
  all_op_types: set[str] = set()
  arch_data: dict[str, dict] = {}
  # First pass: parse XML for all architectures
  for arch, cfg in ARCHS.items():
    print(f"Parsing XML: {cfg['xml']} -> {arch}")
    encodings, enums, types, fmts, op_types_set, suffix_only_ops = parse_xml(cfg["xml"])
    for fmt, ops in FIXES.get(arch, {}).items(): enums.setdefault(fmt, {}).update(ops)
    for fmt, fields in FIELD_FIXES.get(arch, {}).items():
      if fmt in encodings: encodings[fmt] = (encodings[fmt][0] + fields, encodings[fmt][1])
    arch_data[arch] = {"encodings": encodings, "enums": enums, "types": types, "suffix_only_ops": suffix_only_ops}
    for fmt, bits in fmts.items():
      assert fmt not in all_fmts or all_fmts[fmt] == bits, f"FMT_BITS mismatch for {fmt}: {all_fmts[fmt]} vs {bits}"
      all_fmts[fmt] = bits
    all_op_types.update(op_types_set)
  # Write common.py
  autogen_base = pathlib.Path(__file__).parents[2] / "runtime" / "autogen" / "amd"
  common_path = autogen_base / "common.py"
  write_common(all_fmts, all_op_types, common_path)
  print(f"Wrote common.py: {len(all_fmts)} formats, {len(all_op_types)} op types")
  # Write per-arch files from XML
  for arch, data in arch_data.items():
    base = autogen_base / arch
    write_enum(data["enums"], base / "enum.py")
    write_ins(data["encodings"], data["enums"], data["suffix_only_ops"], data["types"], arch, base / "ins.py")
    write_operands(data["types"], data["enums"], arch, base / "operands.py")
    print(f"  {arch}: {len(data['encodings'])} encodings, {sum(len(v) for v in data['enums'].values())} instructions")
  # Second pass: parse PDFs and write pcode
  for arch, cfg in ARCHS.items():
    print(f"Parsing PDF: {arch}...")
    pages = extract_pdf_text(cfg["pdf"])
    name_to_op = {name: op for ops in arch_data[arch]["enums"].values() for op, name in ops.items()}
    pcode = extract_pcode(pages, name_to_op)
    base = autogen_base / arch
    write_pcode(pcode, arch_data[arch]["enums"], arch, base / "str_pcode.py")
    print(f"  {arch}: {len(pcode)} pcode entries")
