#!/usr/bin/env python3

import os
import sys
import subprocess
import traceback

objdump = os.getenv('CROSS','')+'objdump'

host = sys.argv[1]

def warn(w):
  sys.stderr.write('[31;1mwarning: %s[0m\n' % w)
  fresult.write('%s\n' % w)

# ===== setting up capstone

def capstone_disassembler():
  try:
    import capstone
  except:
    warn('this script does not know how to check instruction-set extensions without python3-capstone')
    sys.exit(0)

  # XXX: for arm32 should trace from entry points to see thumb vs. arm
  capstoneopts = {
    'amd64':        ('CS_ARCH_X86','CS_MODE_64','CS_MODE_LITTLE_ENDIAN'),
    'arm32':        ('CS_ARCH_ARM','CS_MODE_THUMB','CS_MODE_LITTLE_ENDIAN'),
    'arm64':        ('CS_ARCH_ARM64','CS_MODE_LITTLE_ENDIAN'),
    # 'm68k':         ('CS_ARCH_M68K','CS_MODE_M68K_040','CS_MODE_BIG_ENDIAN'),
    'mips32big':    ('CS_ARCH_MIPS','CS_MODE_MIPS32','CS_MODE_BIG_ENDIAN'),
    'mips32little': ('CS_ARCH_MIPS','CS_MODE_MIPS32','CS_MODE_LITTLE_ENDIAN'),
    'mips64big':    ('CS_ARCH_MIPS','CS_MODE_MIPS64','CS_MODE_BIG_ENDIAN'),
    'mips64little': ('CS_ARCH_MIPS','CS_MODE_MIPS64','CS_MODE_LITTLE_ENDIAN'),
    'mipsn32big':   ('CS_ARCH_MIPS','CS_MODE_MIPS32','CS_MODE_BIG_ENDIAN'),
    'mipsn32little':('CS_ARCH_MIPS','CS_MODE_MIPS32','CS_MODE_LITTLE_ENDIAN'),
    'ppc32big':     ('CS_ARCH_PPC','CS_MODE_32','CS_MODE_BIG_ENDIAN'),
    'ppc32little':  ('CS_ARCH_PPC','CS_MODE_32','CS_MODE_LITTLE_ENDIAN'),
    'ppc64big':     ('CS_ARCH_PPC','CS_MODE_64','CS_MODE_BIG_ENDIAN'),
    'ppc64little':  ('CS_ARCH_PPC','CS_MODE_64','CS_MODE_LITTLE_ENDIAN'),
    'riscv32':      ('CS_ARCH_RISCV','CS_MODE_RISCV64','CS_MODE_RISCVC'),
    'riscv64':      ('CS_ARCH_RISCV','CS_MODE_RISCV32','CS_MODE_RISCVC'),
    's390':         ('CS_ARCH_SYSZ','CS_MODE_BIG_ENDIAN'),
    's390x':        ('CS_ARCH_SYSZ','CS_MODE_BIG_ENDIAN'),
    'sparc32':      ('CS_ARCH_SPARC','CS_MODE_BIG_ENDIAN'),
    'sparc64':      ('CS_ARCH_SPARC','CS_MODE_V9','CS_MODE_BIG_ENDIAN'),
    'x86':          ('CS_ARCH_X86','CS_MODE_32','CS_MODE_LITTLE_ENDIAN'),
  }

  if host not in capstoneopts:
    warn(f'this script does not know how to check instruction-set extensions for {host}')
    sys.exit(0)

  try:
    bigendian = False
    capstonearch = capstone.__getattribute__(capstoneopts[host][0])
    capstonemode = 0
    for opt in capstoneopts[host][1:]:
      if opt == 'CS_MODE_BIG_ENDIAN': bigendian = True
      capstonemode |= capstone.__getattribute__(opt)
    disassembler = capstone.Cs(capstonearch,capstonemode)
    disassembler.detail = True
  except Exception as e:
    warn(f'failed to initialize python3-capstone: {e}')
    sys.exit(0)

  # script checks these overrides against objdump mnemonics
  # and against capstone mnemonics
  capstoneoverrides = {
    'amd64':{
      'xgetbv':{'xsave'},
      'vpmadd52huq':{'avx512vl','avx512ifma'},
      'vpmadd52luq':{'avx512vl','avx512ifma'},
    },
    'arm64':{
      'aese':{'aes'},
      'aesd':{'aes'},
      'aesmc':{'aes'},
      'aesimc':{'aes'},
      'sha1c':{'sha1'},
      'sha1p':{'sha1'},
      'sha1m':{'sha1'},
      'sha1h':{'sha1'},
      'sha1u0':{'sha1'},
      'sha1u1':{'sha1'},
      'sha256h':{'sha256'},
      'sha256h2':{'sha256'},
      'sha256u0':{'sha256'},
      'sha256u1':{'sha256'},
      'sha512h':{'sha512'},
      'sha512h2':{'sha512'},
      'sha512u0':{'sha512'},
      'sha512u1':{'sha512'},
    },
  }
  capstonealiases = {
    'amd64':{
      'int':'interrupt',
      'sse1':'sse',
      'novlx':'avx',
      'vlx':'avx512vl',
      'dqi':'avx512dq',
      'bmi':'bmi1',
      'avx512':'avx512f',
    }
  }
  # base: instruction sets that can be assumed
  # (see also cross-arch list below)
  # script checks base after applying aliases
  capstonebase = {
    'amd64':{'fpu','cmov','sse','sse2','mode64'},
    'arm32':{'int','v5t','v6','v6m','prev8','thumb','thumb1only','thumb2','T2EXTRACTPACK','THUMB2DSP','mulops'},
    'arm64':{'neon','fparmv8','pointer authentication'},
    'mips64big':{'stdenc','mips3','mips32','mips4_32','mips32r2','mips64r2','notmips32r6','notmips64r6','notinmicromips'},
    'mips64little':{'stdenc','mips3','mips32','mips4_32','mips32r2','mips64r2','notmips32r6','notmips64r6','notinmicromips'},
    'ppc64big':{'mode32','vsx','altivec'},
    'ppc64little':{'mode32','vsx','altivec','p8altivec','p8vector'},
    's390':{'distinctops','highword'},
    's390x':{'distinctops','highword','loadstoreoncond'},
    'sparc64':{'64bit','v9'},
    'x86':{'mode32','fpu','cmov','not64bitmode'},
  }

  overrides = capstoneoverrides.get(host,{})
  aliases = capstonealiases.get(host,{})

  base = capstonebase.get(host,set())
  for ext in 'call','ret','return','jump','branch_relative','privilege','interrupt':
    base.add(ext)

  extrules = aliases,base
  return disassembler,bigendian,overrides,extrules

# ===== main work

carch = {}
for c in os.listdir('compilerarch'):
  with open('compilerarch/%s' % c) as f:
    carch[c] = f.read().strip()
    if carch[c] == 'default': carch[c] = host
    assert carch[c].split('+')[0] == host

def checkext(obj,archlist,ext,extrules,insnstr):
  aliases,base = extrules
  if ext in aliases: ext = aliases[ext]
  extset.add(ext)
  if ext in base: return
  for arch in archlist:
    if ext not in arch.split('+')[1:]:
      warn(f'{obj}: {arch} instruction set does not allow {ext} for {insnstr}')

def dir2objs(d):
  objs = [d+'/'+fn for fn in os.listdir(d) if fn.endswith('.o')]
  if os.path.exists(d+'/dependencies'):
    with open(d+'/dependencies') as f:
      for line in f:
        objs += dir2objs(line.strip())
  return objs

def doit(d):
  disassembler,bigendian,overrides,extrules = capstone_disassembler()

  objs = dir2objs(d)
  if len(objs) == 0: return

  if len(d.split('/')) < 4:
    archlist = [host] # no instruction-set extensions allowed
  else:
    compiler = d.split('/')[3]
    if compiler in carch:
      archlist = [carch[compiler]]
    else:
      archlist = []
      with open(d+'/compilerdirs') as f:
        for arch in f:
          archlist += [carch[arch.strip()]]

  try:
    # XXX: think about how to safely exclude glink section on ppc64
    p = subprocess.Popen([objdump,'-d']+objs,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,universal_newlines=True)
    out,err = p.communicate()
  except Exception as e:
    warn('objdump exception: %s' % e)
    return
  if err:
    warn('objdump error: %s' % err)
    return
  if p.returncode:
    warn('objdump failure: %s' % p.returncode)
    return

  obj = 'unknown-object-file'
  todo = b''
  # on x86 and amd64, objdump can split insns across lines
  # and todo accumulates instruction bytes not yet processed

  for line in out.splitlines():
    if len(objs) > 0 and line.startswith('%s:'%objs[0]):
      obj,objs = objs[0],objs[1:]
    if '\t' in line:
      line = line[line.index('\t')+1:]
      if '#' in line:
        line = line[:line.index('#')]
      if '<' in line:
        line = line[:line.index('<')]

      for x in line.split():
        try:
          x = bytes.fromhex(x)
        except:
          if x in overrides:
            for ext in overrides[x]:
              checkext(obj,archlist,ext,extrules,f'{x} (from objdump)')
          break
        if not bigendian:
          x = bytes(reversed(bytearray(x)))
        todo += x

      vbroadcasti128 = (
        'c4 e2 7d 5a 02',
        'c4 62 7d 5a 1a',
        'c4 62 7d 5a 2a',
        'c4 62 7d 5a 5a 10',
        'c4 62 7d 5a 5a 20',
        'c4 62 7d 5a 5a 30',
        'c4 62 7d 5a 5a 40',
        'c4 62 7d 5a 5a 50',
        'c4 62 7d 5a 5a 60',
        'c4 62 7d 5a 5a 70',
        'c4 e2 7d 5a 82 b0 00 00 00',
        'c4 62 7d 5a 9a 80 00 00 00',
        'c4 62 7d 5a 9a 90 00 00 00',
        'c4 62 7d 5a 9a a0 00 00 00',
      )
      if host == 'amd64' and any(todo == bytes.fromhex(x) for x in vbroadcasti128):
        checkext(obj,archlist,'avx2',extrules,f'vbroadcasti128 (from checkinsns)')
        todo = b''
        continue

      for insn in disassembler.disasm(todo,0x1000):
        assert todo.startswith(insn.bytes)
        todo = todo[len(insn.bytes):]
        if insn.mnemonic in overrides:
          for ext in overrides[insn.mnemonic]:
            checkext(obj,archlist,ext,extrules,f'{insn.mnemonic} {insn.op_str} (from capstone)')
        else:
          for g in insn.groups:
            checkext(obj,archlist,insn.group_name(g),extrules,f'{insn.mnemonic} {insn.op_str} (from capstone)')

      if host not in ('x86','amd64'):
        if todo != b'':
          warn(f'capstone disassembly failed, could be capstone bug: {line}')
          todo = b''
      if len(todo) > 32:
        warn(f'capstone disassembly has built up more than 32 bytes, resetting')
        todo = b''

for d in sys.argv[2:]:
  d = d.strip()
  extset = set()
  with open('%s/result-insns'%d,'w') as fresult:
    try:
      doit(d)
    except SystemExit as e:
      sys.exit(0)
    except:
      warn(f'checking instructions failed:\n{traceback.format_exc()}')
  with open('%s/info-insns'%d,'w') as finfo:
    for ext in sorted(extset):
      finfo.write(ext+'\n')
