#!/usr/bin/env python3 """Convert smart34010 (dis34 -o) output into TI gspa/as34 2.00 syntax. Usage: dis2as34.py BASE_HEX SIZE_BYTES INPUT.ASM > OUT.ASM The input must be produced with ``dis34 -o`` (opcode words in a trailing comment) because the 1987 assembler is *not* byte-faithful on its own: * ``MOVI`` is one mnemonic with a short (16-bit, sign-extended) and a long (32-bit) form. gspa auto-selects by "does the value fit in 16 bits", so a long MOVI of 0x53E0 (which the ROM encodes as a 3-word instruction) is silently shortened -- and sign extension then changes its meaning. The explicit ``W`` / ``L`` operand forces the form. * the same applies to ADDI/CMPI/SUBI. * dis34 prints a *long relative* JR (16-bit displacement) as ``ja``, the same mnemonic it uses for the 3-word absolute JA. The opcode word count disambiguates: 2 words = long JR, 3 words = absolute JA. * dis34 prints B-file register 15 as ``sp`` too; gspa's bare ``sp`` is the A-file register. B-file SP has to be written ``b15`` (the assembler rejects ``a15``). * register-indirect displacements are signed 16-bit fields. dis34 prints the raw two's-complement bits (0xFFE0); gspa rejects those as out of bounds, so they are rendered as signed decimal (-32). * constants: dis34 emits ``0xHHHH``; gspa wants ``0HHHHh`` (must start with a decimal digit, so 0xC00000B0 becomes 0C00000B0h). * a PC-relative branch whose target is not an instruction boundary in the region (outside it, or -- as happens in data tables -- inside a multi-word instruction) cannot be expressed as a label. gspa also mishandles a negative absolute ``.set`` target in a JR. The exact opcode words are therefore emitted as ``.word`` for those branches. """ import re import sys DISP = re.compile(r"(\*\s*[ab]\d+|\*\s*sp)\(\s*0x([0-9a-fA-F]+)\s*\)") HEX = re.compile(r"0x([0-9a-fA-F]+)") FORM_OPS = {"movi", "addi", "cmpi", "subi"} RELATIVE = re.compile(r"^(jreq|jrne|jrlt|jrle|jrgt|jrge|jrc|jrnc|jrlo|jrls|jrhi|jrhs|jrnz|jrz|" r"jruc|jrxn|jrxnn|jrxz|jrxeq|jrxgt|jrxle|jryn|jrynn|jryz|jryeq|jrygt|jryle|" r"call|callr)$") DSJ_OPS = {"dsj", "dsjs", "dsjeq", "dsjne"} def ti_hex(digits: str) -> str: return "0" + digits.upper() + "h" def fix_disp(m): base, digits = m.group(1), m.group(2) v = int(digits, 16) if v >= 0x8000: return f"{base}({v - 0x10000})" return f"{base}(0{digits.upper()}h)" def raw_words(words): return " .word " + ", ".join(ti_hex(w) for w in words) def convert(src: str, base: int, size: int) -> str: end = base + size * 8 lines = src.splitlines() defined = set() for line in lines: code = line.partition(";")[0] dm = re.match(r"^(L[0-9A-Fa-f]{8}):", code) if dm: defined.add(dm.group(1).upper()) out = [] referenced = set() for line in lines: code, sep, comment = line.partition(";") for lm in re.findall(r"\b(L[0-9A-Fa-f]{8})\b", code): referenced.add(lm.upper()) words = re.findall(r"\b[0-9a-fA-F]{4}\b", comment) stripped = code.rstrip() # long relative JR printed as JA (see module docstring) mm = re.match(r"^(\s*)ja([a-z]+)(\s+)(\S.*)$", stripped) if mm and len(words) == 2: stripped = f"{mm.group(1)}jr{mm.group(2)}{mm.group(3)}{mm.group(4)}" parts = stripped.split() op = parts[0].lower() if parts else "" # B-file SP (see docstring) if op == "rl" and words and stripped.endswith(", sp"): if int(words[0], 16) & 0x1f == 0x1f: stripped = stripped[:-3] + "b15" # short/long immediate forms (see docstring) if op in FORM_OPS and words: if len(words) == 2: stripped += ", W" mm = re.match(r"^(\s*\w+\s+)0x([0-9a-fA-F]+)(,.*)$", stripped) if mm and int(mm.group(2), 16) >= 0x8000: stripped = (mm.group(1) + str(int(mm.group(2), 16) - 0x10000) + mm.group(3)) elif len(words) == 3: stripped += ", L" # relative branch to a label that is not an instruction boundary mm = re.match(r"^(\s*)(\w+)(\s+.*?)(L[0-9A-Fa-f]{8})\s*$", stripped) if mm and mm.group(4).upper() not in defined: op2 = mm.group(2).lower() if op2 in DSJ_OPS or RELATIVE.match(op2): stripped = raw_words(words) # relative branch to an absolute address outside the region mm = re.match(r"^(\s*)([a-z]+)(\s+)0x([0-9a-fA-F]+)\s*$", stripped) if mm and RELATIVE.match(mm.group(2).lower()): target = int(mm.group(4), 16) if not (base <= target < end): stripped = raw_words(words) stripped = DISP.sub(fix_disp, stripped) stripped = HEX.sub(lambda mm: ti_hex(mm.group(1)), stripped) out.append(stripped) missing = sorted(referenced - defined) if missing: out.append("") for name in missing: off = int(name[1:], 16) - base out.append(f"{name} .set " + ("0%Xh" % off if off >= 0 else str(off))) return "\n".join(out) + "\n" if __name__ == "__main__": base = int(sys.argv[1], 16) size = int(sys.argv[2]) sys.stdout.write(convert(open(sys.argv[3]).read(), base, size))