87 lines
3.7 KiB
Python
87 lines
3.7 KiB
Python
"""A tiny TMS9900 machine-code emitter (just the instructions the TI-99 viewer
|
|
needs). Words are big-endian. Supports labels + relative-jump backpatching so
|
|
the viewer's loops can be written readably.
|
|
|
|
This is the TI analogue of using `xa` for the 6502 viewers, but since no TMS9900
|
|
assembler is installed we emit the opcodes directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class Asm:
|
|
def __init__(self, base: int):
|
|
self.base = base
|
|
self.code = bytearray()
|
|
self.labels: dict[str, int] = {}
|
|
self._jfix: list[tuple[int, str]] = [] # (pos, label) for 8-bit jump disp
|
|
|
|
# ---- low level ----
|
|
def pos(self) -> int:
|
|
return self.base + len(self.code)
|
|
|
|
def w(self, word: int):
|
|
self.code += bytes([(word >> 8) & 0xFF, word & 0xFF]) # big-endian
|
|
|
|
def label(self, name: str):
|
|
self.labels[name] = self.pos()
|
|
|
|
# ---- immediate-format (format VIII) ----
|
|
def li(self, reg, imm): self.w(0x0200 | reg); self.w(imm & 0xFFFF)
|
|
def ai(self, reg, imm): self.w(0x0220 | reg); self.w(imm & 0xFFFF)
|
|
def ci(self, reg, imm): self.w(0x0280 | reg); self.w(imm & 0xFFFF)
|
|
def limi(self, imm): self.w(0x0300); self.w(imm & 0xFFFF)
|
|
def lwpi(self, imm): self.w(0x02E0); self.w(imm & 0xFFFF)
|
|
|
|
# ---- single-register (format VI) ----
|
|
def clr(self, reg): self.w(0x04C0 | reg)
|
|
def inc(self, reg): self.w(0x0580 | reg)
|
|
def inct(self, reg): self.w(0x05C0 | reg)
|
|
def dec(self, reg): self.w(0x0600 | reg)
|
|
def dect(self, reg): self.w(0x0640 | reg)
|
|
def swpb(self, reg): self.w(0x06C0 | reg)
|
|
|
|
# ---- two-operand (format I); modes: 0=reg,1=*reg,2=@addr(reg),3=*reg+ ----
|
|
def _fmt1(self, base, td, dreg, ts, sreg, saddr=None, daddr=None):
|
|
self.w(base | ((td & 3) << 10) | ((dreg & 15) << 6)
|
|
| ((ts & 3) << 4) | (sreg & 15))
|
|
if ts == 2:
|
|
self.w(saddr & 0xFFFF)
|
|
if td == 2:
|
|
self.w(daddr & 0xFFFF)
|
|
|
|
def mov_rr(self, s, d): self._fmt1(0xC000, 0, d, 0, s)
|
|
def movb_r_sym(self, s, addr): self._fmt1(0xD000, 2, 0, 0, s, daddr=addr)
|
|
def movb_sym_r(self, addr, d): self._fmt1(0xD000, 0, d, 2, 0, saddr=addr)
|
|
def movb_sinc_sym(self, s, addr): self._fmt1(0xD000, 2, 0, 3, s, daddr=addr)
|
|
def movb_sinc_r(self, s, d): self._fmt1(0xD000, 0, d, 3, s)
|
|
def movb_r_r(self, s, d): self._fmt1(0xD000, 0, d, 0, s)
|
|
|
|
# ---- immediate logic / context switch ----
|
|
def andi(self, reg, imm): self.w(0x0240 | reg); self.w(imm & 0xFFFF)
|
|
def ori(self, reg, imm): self.w(0x0260 | reg); self.w(imm & 0xFFFF)
|
|
def blwp_sym(self, addr): self.w(0x0420); self.w(addr & 0xFFFF) # BLWP @addr
|
|
|
|
# ---- CRU (keyboard scan): R12 holds the CRU base ----
|
|
def ldcr(self, reg, count): self.w(0x3000 | ((count & 15) << 6) | reg)
|
|
def stcr(self, reg, count): self.w(0x3400 | ((count & 15) << 6) | reg)
|
|
|
|
# ---- jumps (format II), 8-bit signed displacement ----
|
|
def _jump(self, opbase, label):
|
|
self._jfix.append((len(self.code), label))
|
|
self.w(opbase) # disp filled in by resolve()
|
|
|
|
def jmp(self, label): self._jump(0x1000, label)
|
|
def jeq(self, label): self._jump(0x1300, label)
|
|
def jne(self, label): self._jump(0x1600, label)
|
|
|
|
# ---- finish ----
|
|
def resolve(self) -> bytes:
|
|
for pos, label in self._jfix:
|
|
target = self.labels[label]
|
|
here = self.base + pos # address of the jump word
|
|
disp = (target - (here + 2)) // 2
|
|
if not -128 <= disp <= 127:
|
|
raise ValueError(f"jump to {label} out of range ({disp})")
|
|
self.code[pos + 1] = disp & 0xFF
|
|
return bytes(self.code)
|