8bitlenser/lenser/ti99/tms9900.py
2026-07-03 19:35:35 -07:00

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)