mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
The 1987 DOS toolchain (GSPA/GSPLNK/GSPROM) runs under DOSBox and reassembles 32 KB of the v2.2 ROM byte-for-byte after five documented fixups to smart34010 output. The 1990 r5.01 tools are OS/2 NE and do not run in DOS. Adds the converter, round-trip script, and the DOSBox/dosemu benchmark.
126 lines
5.3 KiB
Python
126 lines
5.3 KiB
Python
#!/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<cc>``, 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))
|