8bitlenser/lenser/coleco/z80.py
2026-07-03 19:35:35 -07:00

112 lines
4.1 KiB
Python

"""A tiny Z80 machine-code emitter (just what the ColecoVision/Adam viewer needs).
No Z80 assembler is installed, so -- as with the TMS9900 and 6809 emitters -- we
emit opcodes directly. Little-endian 16-bit operands; 8-bit relative jumps are
backpatched via labels.
"""
from __future__ import annotations
class Asm:
def __init__(self, base: int):
self.base = base
self.code = bytearray()
self.labels: dict[str, int] = {}
self._fix: list[tuple[int, str]] = [] # (pos of rel byte, label)
def pos(self) -> int:
return self.base + len(self.code)
def label(self, name: str):
self.labels[name] = self.pos()
def _b(self, *bs):
self.code += bytes(b & 0xFF for b in bs)
def _w(self, v): # 16-bit, little-endian
self.code += bytes([v & 0xFF, (v >> 8) & 0xFF])
# ---- 8-bit loads (immediate) ----
def ld_a(self, n): self._b(0x3E, n)
def ld_b(self, n): self._b(0x06, n)
def ld_c(self, n): self._b(0x0E, n)
def ld_d(self, n): self._b(0x16, n)
def ld_e(self, n): self._b(0x1E, n)
# ---- 16-bit loads (immediate) ----
def ld_hl(self, nn): self._b(0x21); self._w(nn)
def ld_de(self, nn): self._b(0x11); self._w(nn)
def ld_bc(self, nn): self._b(0x01); self._w(nn)
# ---- memory <-> A (extended addressing) ----
def ld_a_mem(self, addr): self._b(0x3A); self._w(addr) # LD A,(nn)
def ld_mem_a(self, addr): self._b(0x32); self._w(addr) # LD (nn),A
# ---- register moves ----
def ld_a_b(self): self._b(0x78)
def ld_a_c(self): self._b(0x79)
def ld_a_d(self): self._b(0x7A)
def ld_a_e(self): self._b(0x7B)
def ld_a_h(self): self._b(0x7C)
def ld_a_l(self): self._b(0x7D)
def ld_b_a(self): self._b(0x47)
def ld_c_a(self): self._b(0x4F)
def ld_a_hl(self): self._b(0x7E) # LD A,(HL)
def ld_hl_a(self): self._b(0x77) # LD (HL),A
# ---- I/O ----
def out_n_a(self, n): self._b(0xD3, n) # OUT (n),A
def in_a_n(self, n): self._b(0xDB, n) # IN A,(n)
# ---- arithmetic / logic ----
def inc_a(self): self._b(0x3C)
def dec_a(self): self._b(0x3D)
def dec_c(self): self._b(0x0D)
def inc_hl(self): self._b(0x23)
def dec_hl(self): self._b(0x2B)
def inc_de(self): self._b(0x13)
def dec_de(self): self._b(0x1B)
def dec_bc(self): self._b(0x0B)
def or_a(self): self._b(0xB7) # OR A (set flags from A)
def or_c(self): self._b(0xB1)
def or_e(self): self._b(0xB3)
def and_n(self, n): self._b(0xE6, n)
def cp_n(self, n): self._b(0xFE, n)
def add_hl_de(self): self._b(0x19)
# ---- block ops ----
def ldir(self): self._b(0xED, 0xB0) # (HL)->(DE), BC times
# ---- control ----
def di(self): self._b(0xF3)
def ei(self): self._b(0xFB)
def ret(self): self._b(0xC9)
def retn(self): self._b(0xED, 0x45)
def nop(self): self._b(0x00)
def jp(self, addr): self._b(0xC3); self._w(addr)
def jp_label(self, lbl):
self._b(0xC3); self._fix.append((len(self.code), lbl)); self._w(0)
def _jr(self, op, lbl):
self._b(op); self._fix.append((len(self.code), lbl)); self._b(0)
def jr(self, lbl): self._jr(0x18, lbl)
def jr_nz(self, lbl): self._jr(0x20, lbl)
def jr_z(self, lbl): self._jr(0x28, lbl)
def djnz(self, lbl): self._jr(0x10, lbl)
def resolve(self) -> bytes:
for pos, lbl in self._fix:
target = self.labels[lbl]
opcode = self.code[pos - 1] # the byte just before the operand
if opcode == 0xC3: # JP nn (absolute, 2 bytes)
self.code[pos] = target & 0xFF
self.code[pos + 1] = (target >> 8) & 0xFF
else: # JR / DJNZ (relative, 1 byte)
rel = target - (self.base + pos + 1)
if not -128 <= rel <= 127:
raise ValueError(f"relative jump to {lbl} out of range ({rel})")
self.code[pos] = rel & 0xFF
return bytes(self.code)