diff --git a/src/outfmt/__init__.py b/src/outfmt/__init__.py
index 03501b5ab..f35c15ddd 100644
--- a/src/outfmt/__init__.py
+++ b/src/outfmt/__init__.py
@@ -5,11 +5,13 @@
from .sna import SnaEmitter
from .tap import TAP
from .tzx import TZX
+from .z80 import Z80Emitter
__all__ = (
"BinaryEmitter",
"CodeEmitter",
- "TZX",
- "TAP",
"SnaEmitter",
+ "TAP",
+ "TZX",
+ "Z80Emitter",
)
diff --git a/src/outfmt/gensnapshot.py b/src/outfmt/gensnapshot.py
new file mode 100644
index 000000000..918e4a238
--- /dev/null
+++ b/src/outfmt/gensnapshot.py
@@ -0,0 +1,271 @@
+#!/usr/bin/env python3
+
+# 48K Snapshot generation module
+#
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file AUTHORS for copyright details.
+#
+# This file is part of Boriel BASIC Compiler.
+#
+# Boriel BASIC Compiler is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# Boriel BASIC Compiler is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Boriel BASIC Compiler. If not, see .
+
+
+class GenSnapshot:
+ """Generate 48K snapshots with the given BASIC and MC code
+
+ If no BASIC is given, it will insert its own BASIC program, consisting of
+ a single line that reads:
+
+ 10 IF USR THEN
+ """
+
+ # Utility functions
+ @staticmethod
+ def word(x: int):
+ """Convert an integer to an iterable of 2 little-endian bytes"""
+ return bytes((x % 256, x >> 8))
+
+ def patchAddr(self, addr: int, data: bytes):
+ """Patch the snapshot's memory image at the given address"""
+ self.mem[addr - 16384 : addr - 16384 + len(data)] = data
+ assert len(self.mem) == 49152
+
+ def __init__(
+ self,
+ loader_bytes,
+ clear_addr,
+ mc_addr,
+ mc_bytes,
+ ):
+ """
+ Creates a snapshot object ready to run a BASIC program as if RUN was just executed.
+
+ Input:
+
+ loader_bytes: ZX Spectrum BASIC code to inject as the BASIC program.
+ If None, a program consisting of this single line will be generated:
+ 10 IF USR THEN
+ clear_addr: Address of CLEAR
+ mc_addr: Address where the bytes need to be stored
+ mc_bytes: Bytes to store starting at mc_addr
+
+ Output: An object with the following fields:
+
+ mem: A bytearray object with the memory dump
+ A: the Z80 A register
+ B, C, D, E, H, L, F, A2, B2, C2, D2, E2, H2, L2, F2, I, R: same
+ W: high byte of the WZ internal register
+ Z: low byte of the WZ internal register
+ SPH: High byte of SP
+ SPL: Low byte of SP
+ PCH: High byte of PC
+ PCL: Low byte of PC
+ IXH: High byte of IX
+ IXL: Low byte of IX
+ IYH: High byte of IY
+ IYL: Low byte of IY
+ IFF1: IFF1 flag of Z80 (0 to 1, as int)
+ IFF2: IFF2 flag of Z80 (0 to 1, as int)
+ outFE: Last byte output to port 0FEh
+ IM: Interrupt mode (0 to 2, as int)
+ cycles: Cycles after the last interrupt
+ halted: Whether we're in a halted state
+ eilast: Whether the last instruction prevents an interrupt
+ """
+
+ self.A = self.A2 = self.B = self.B2 = self.C = self.C2 = self.D = self.D2 = self.E = self.E2 = self.H = (
+ self.H2
+ ) = self.L = self.L2 = self.F = self.F2 = self.R = self.IXL = self.IXH = 0
+
+ self.IYH = 0x5C
+ self.IYL = 0x3A # 0x5C3A is the normal value of IY for ROM use
+ self.I = 0x3F
+ self.W = 0
+ self.Z = 0
+ self.IFF1 = 1
+ self.IFF2 = 1
+ self.PCH = 0x1B
+ self.PCL = 0x9E # Entry point: 1B9E, LINE_NEW
+ SP = clear_addr - 3
+ self.SPH = (SP >> 8) & 0xFF
+ self.SPL = SP & 0xFF
+ self.IM = 1
+ self.outFE = 0x0F # Border 7, input enabled, speaker disabled
+ self.cycles = 35000 # Half a screen, roughly
+ self.halted = False
+ self.eilast = False
+
+ # Build a valid memory image from scratch
+ # Start with an array of 49152 zeros, then patch the different areas
+ self.mem = bytearray(b"\0" * 49152)
+
+ # Screen Attributes
+ self.patchAddr(0x5800, b"\x38" * 768) # all Paper 7 / Ink 0
+
+ # System Variables
+ # The author knows very little about KSTATE, so just in case, the
+ # eight state bytes have been copied from an actual snapshot.
+ self.patchAddr(
+ 0x5C00,
+ b"\xff\0\0\0"
+ b"\x0d\2\x20\x0d" # KSTATE
+ b"\x0d" # LAST_K
+ b"\x23" # REPDEL
+ b"\x05" # REPPER
+ b"\0\0\0\0\0" # DEFADD, K_DATA, TVDATA
+ b"\1\0\6\0\x0b\0\1\0\1\0\6\0\x10\0"
+ b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
+ b"\0\0\0\0\0\0\0\0" # STRMS (38 bytes total)
+ b"\x00\x3c" # CHARS
+ b"\x40\x00" # RASP, PIP
+ b"\xff" # ERR_NR
+ b"\xcc" # FLAGS
+ b"\x00" # TV_FLAG
+ b"\0\0" # ERR_SP (to be patched with clear addr - 3)
+ b"\0\0" # LIST_SP (overwritten by ROM)
+ b"\0" # MODE
+ b"\0\0\0" # NEWPPC, NSPPC (at start of BASIC)
+ b"\xfe\xff\1" # PPC, SUBPPC (at line -2, edit line)
+ b"\x38" # BORDCR
+ b"\0\0" # E_PPC
+ b"\0\0" # VARS (patched later, depends on prog length)
+ b"\0\0" # DEST
+ b"\xb6\x5c" # CHANS
+ b"\xb6\x5c" # CURCHL
+ b"\xcb\x5c" # PROG
+ b"\0\0" # NXTLIN (overwritten by ROM)
+ b"\xca\x5c" # DATADD
+ b"\0\0" # E_LINE (patched later)
+ b"\0\0" # K_CUR (overwritten by ROM)
+ b"\0\0" # CH_ADD (overwritten by ROM)
+ b"\0\0" # X_PTR
+ b"\0\0" # WORKSP (patched later)
+ b"\0\0" # STKBOT (patched later)
+ b"\0\0" # STKEND (patched later)
+ b"\0" # BREG
+ b"\x92\x5c" # MEM
+ b"\x10" # FLAGS2
+ b"\2" # DF_SZ
+ b"\0\0\0\0\0" # S_TOP, OLDPPC, OSPPC
+ b"\0\0\0" # FLAGX, STRLEN
+ b"\0\0" # T_ADDR (overwritten by ROM)
+ b"\0\0" # SEED
+ b"\0\0\0" # FRAMES
+ b"\x58\xff" # UDG
+ b"\0\0" # COORDS
+ b"\x21" # P_POSN
+ b"\0\x5b" # PR_CC
+ b"\x21\x17" # ECHO_E
+ b"\0\x40" # DF_CC
+ b"\xe0\x50" # DFCCL
+ b"\x21\x18" # S_POSN
+ b"\x21\x17" # SPOSNL
+ b"\1" # SCR_CT
+ b"\x38\x00\x38\x00" # ATTR_P, MASK_P, ATTR_T, MASK_T
+ b"\0" # P_FLAG
+ b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
+ b"\0\0\0\0\0\0\0\0" # MEMBOT (30 bytes)
+ b"\0\0" # NMIADD
+ b"\0\0" # RAMTOP (patched later)
+ b"\xff\xff", # P_RAMT
+ )
+
+ ChansData = (
+ # CHANS data (routine pointers, channel name)
+ b"\xf4\x09\xa8\x10"
+ b"K" # PRINT_OUT, KEY_INPUT, "K"
+ b"\xf4\x09\xc4\x15"
+ b"S" # PRINT_OUT, REPORT_J, "S"
+ b"\x81\x0f\xc4\x15"
+ b"R" # ADD_CHAR, REPORT_J, "R"
+ b"\xf4\x09\xc4\x15"
+ b"P" # PRINT_OUT, REPORT_J, "P"
+ b"\x80" # Terminator
+ )
+
+ # CHANS data starts at 23734 and could vary in length depending on
+ # the presence of an Interface 1.
+ self.patchAddr(23734, ChansData)
+
+ # BASIC start (usually 23755 in absence of Interface 1)
+ self.BasicStart = 23734 + len(ChansData)
+
+ if loader_bytes is None:
+ # Create a single line with these contents: 10 IF USR THEN
+ loader_bytes = bytearray(
+ b"\0\x0a" # BASIC big endian line num
+ b"\0\0" # BASIC little endian line length (patched below)
+ b"\xfa\xc0" # BASIC IF USR
+ )
+ loader_bytes.extend(b"%05d\x0e\0\0\0\0\0" % mc_addr)
+ loader_bytes[-3:-1] = self.word(mc_addr)
+ loader_bytes.extend(b"\xcb\x0d") # THEN + final newline
+ loader_bytes[2:4] = self.word(len(loader_bytes) - 4) # line length
+
+ BasicLength = len(loader_bytes)
+
+ # Address to array index conversion offset; 0x1B is the header size
+ BasicEnd = self.BasicStart + BasicLength
+
+ # Clear everything from the channel variables to the UDG start
+ self.patchAddr(self.BasicStart, b"\x00" * (65368 - self.BasicStart))
+
+ # Patch ERR_SP
+ self.patchAddr(23613, self.word(clear_addr - 3))
+ # Patch VARS
+ self.patchAddr(23627, self.word(BasicEnd))
+ # Patch E_LINE
+ self.patchAddr(23641, self.word(BasicEnd + 1))
+ # Patch WORKSP
+ self.patchAddr(23649, self.word(BasicEnd + 4))
+ # Patch STKBOT
+ self.patchAddr(23651, self.word(BasicEnd + 4))
+ # Patch STKEND
+ self.patchAddr(23653, self.word(BasicEnd + 4))
+ # Patch RAMTOP
+ self.patchAddr(23730, self.word(clear_addr))
+
+ # Patch BASIC program
+ self.patchAddr(self.BasicStart, loader_bytes)
+
+ # Patch variables area, edit line and calculator stack (edit line
+ # contains a RUN command, 0xF7; calculator stack is empty)
+ self.patchAddr(BasicEnd, b"\x80\xf7\x0d\x80")
+
+ # Patch stack
+ self.patchAddr(
+ clear_addr - 3,
+ b"\x03\x13" # Error resume routine (ERR_SP points here): MAIN_4
+ b"\x00\x3e", # GOSUB stack end marker
+ )
+
+ # UDG area (might be overwritten by compiled code)
+ self.patchAddr(
+ 65368,
+ b"\x00\x3c\x42\x42\x7e\x42\x42\x00\x00\x7c\x42\x7c\x42\x42\x7c\x00"
+ b"\x00\x3c\x42\x40\x40\x42\x3c\x00\x00\x78\x44\x42\x42\x44\x78\x00"
+ b"\x00\x7e\x40\x7c\x40\x40\x7e\x00\x00\x7e\x40\x7c\x40\x40\x40\x00"
+ b"\x00\x3c\x42\x40\x4e\x42\x3c\x00\x00\x42\x42\x7e\x42\x42\x42\x00"
+ b"\x00\x3e\x08\x08\x08\x08\x3e\x00\x00\x02\x02\x02\x42\x42\x3c\x00"
+ b"\x00\x44\x48\x70\x48\x44\x42\x00\x00\x40\x40\x40\x40\x40\x7e\x00"
+ b"\x00\x42\x66\x5a\x42\x42\x42\x00\x00\x42\x62\x52\x4a\x46\x42\x00"
+ b"\x00\x3c\x42\x42\x42\x42\x3c\x00\x00\x7c\x42\x42\x7c\x40\x40\x00"
+ b"\x00\x3c\x42\x42\x52\x4a\x3c\x00\x00\x7c\x42\x42\x7c\x44\x42\x00"
+ b"\x00\x3c\x40\x3c\x02\x42\x3c\x00\x00\xfe\x10\x10\x10\x10\x10\x00"
+ b"\x00\x42\x42\x42\x42\x42\x3c\x00",
+ )
+
+ # Finally, patch compiled code in
+ assert mc_addr + len(mc_bytes) <= 65536
+ self.patchAddr(mc_addr, mc_bytes)
diff --git a/src/outfmt/sna.py b/src/outfmt/sna.py
index 218c8037c..58fd55e2c 100644
--- a/src/outfmt/sna.py
+++ b/src/outfmt/sna.py
@@ -8,45 +8,35 @@
# This file is part of Boriel BASIC Compiler.
#
# Boriel BASIC Compiler is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option) any
-# later version.
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
#
# Boriel BASIC Compiler is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-# details.
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
#
-# You should have received a copy of the GNU General Public License along with
-# Boriel BASIC Compiler. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with Boriel BASIC Compiler. If not, see .
from .codeemitter import CodeEmitter
+from .gensnapshot import GenSnapshot
class SnaEmitter(CodeEmitter):
- """Class to emit 48K .SNA snapshots"""
-
- # Utility functions
- @staticmethod
- def word(x: int):
- """Convert an integer to an iterable of 2 little-endian bytes"""
- return bytes((x % 256, x >> 8))
-
- def patchAddr(self, addr: int, data: bytes):
- """Patch the snapshot at the given address"""
- self.patchIdx(addr + (0x1B - 16384), data)
-
- def patchIdx(self, idx: int, data: bytes):
- """Patch the snapshot at the given array index"""
- self.output[idx : idx + len(data)] = data
- assert len(self.output) == 49179
-
- def __init__(self):
- """Initializes the base .SNA bytes"""
+ """Class to write 48K .SNA snapshots"""
+ def generate(
+ self,
+ loader_bytes,
+ clear_addr,
+ entry_point,
+ program_bytes,
+ ):
"""
- Format of SNA file:
+ Format of .SNA file:
$00 I
$01 HL'
@@ -65,116 +55,54 @@ def __init__(self):
$19 Interrupt mode: 0, 1 or 2
$1A Border colour
+ A raw memory dump (49152 bytes) follows.
+
PC is on the stack. A `retn` instruction should be executed after load.
"""
- # Start with an array of 49179 zeros, then patch the different areas
- self.output = bytearray(b"\0" * 49179)
-
- # Registers in header
- self.patchIdx(
- 0,
- b"\x3f" # I
- b"\0\0\0\0\0\0\0\0" # HL', DE', BC', AF'
- b"\0\0\0\0\0\0" # HL, DE, BC
- b"\x3a\x5c" # IY
- b"\0\0" # IX
- b"\x04" # Interrupts enabled
- b"\0" # R
- b"\0\0" # AF
- b"\0\0" # SP (to be patched with clear addr - 5)
- b"\1\7", # IM1, Border 7
+ snapshot = GenSnapshot(loader_bytes, clear_addr, entry_point, program_bytes)
+
+ # SNA stores the start address in the stack, so SP should be adjusted
+ SP = ((snapshot.SPH << 8) | snapshot.SPL) - 2
+
+ sna_data = bytearray(
+ (
+ snapshot.I,
+ snapshot.L2,
+ snapshot.H2,
+ snapshot.E2,
+ snapshot.D2,
+ snapshot.C2,
+ snapshot.B2,
+ snapshot.F2,
+ snapshot.A2,
+ snapshot.L,
+ snapshot.H,
+ snapshot.E,
+ snapshot.D,
+ snapshot.C,
+ snapshot.B,
+ snapshot.IYL,
+ snapshot.IYH,
+ snapshot.IXL,
+ snapshot.IXH,
+ 4 if snapshot.IFF1 else 0,
+ snapshot.R,
+ snapshot.F,
+ snapshot.A,
+ SP & 0xFF,
+ SP >> 8,
+ snapshot.IM,
+ snapshot.outFE & 7,
+ )
)
+ snaHeaderLen = len(sna_data)
- # Screen Attributes
- self.patchAddr(0x5800, b"\x38" * 768)
-
- # System Variables
- # The author knows very little about KSTATE, so just in case, the
- # eight state bytes have been copied from an actual snapshot.
- self.patchAddr(
- 0x5C00,
- b"\xff\0\0\0\x0d\2\x20\x0d" # KSTATE
- b"\x0d" # LAST_K
- b"\x23" # REPDEL
- b"\x05" # REPPER
- b"\0\0\0\0\0" # DEFADD, K_DATA, TVDATA
- b"\1\0\6\0\x0b\0\1\0\1\0\6\0\x10\0"
- b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
- b"\0\0\0\0\0\0\0\0" # STRMS (38 bytes total)
- b"\x00\x3c" # CHARS
- b"\x40\x00" # RASP, PIP
- b"\xff" # ERR_NR
- b"\xcc" # FLAGS
- b"\x00" # TV_FLAG
- b"\0\0" # ERR_SP (to be patched with clear addr - 3)
- b"\0\0" # LIST_SP (overwritten by ROM)
- b"\0" # MODE
- b"\0\0\0" # NEWPPC, NSPPC (at start of BASIC)
- b"\xfe\xff\1" # PPC, SUBPPC (at line -2, edit line)
- b"\x38" # BORDCR
- b"\0\0" # E_PPC
- b"\0\0" # VARS (patched later, depends on prog length)
- b"\0\0" # DEST
- b"\xb6\x5c" # CHANS
- b"\xb6\x5c" # CURCHL
- b"\xcb\x5c" # PROG
- b"\0\0" # NXTLIN (overwritten by ROM)
- b"\xca\x5c" # DATADD
- b"\0\0" # E_LINE (patched later)
- b"\0\0" # K_CUR (overwritten by ROM)
- b"\0\0" # CH_ADD (overwritten by ROM)
- b"\0\0" # X_PTR
- b"\0\0" # WORKSP (patched later)
- b"\0\0" # STKBOT (patched later)
- b"\0\0" # STKEND (patched later)
- b"\0" # BREG
- b"\x92\x5c" # MEM
- b"\x10" # FLAGS2
- b"\2" # DF_SZ
- b"\0\0\0\0\0" # S_TOP, OLDPPC, OSPPC
- b"\0\0\0" # FLAGX, STRLEN
- b"\0\0" # T_ADDR (overwritten by ROM)
- b"\0\0" # SEED
- b"\0\0\0" # FRAMES
- b"\x58\xff" # UDG
- b"\0\0" # COORDS
- b"\x21" # P_POSN
- b"\0\x5b" # PR_CC
- b"\x21\x17" # ECHO_E
- b"\0\x40" # DF_CC
- b"\xe0\x50" # DFCCL
- b"\x21\x18" # S_POSN
- b"\x21\x17" # SPOSNL
- b"\1" # SCR_CT
- b"\x38\x00\x38\x00" # ATTR_P, MASK_P, ATTR_T, MASK_T
- b"\0" # P_FLAG
- b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
- b"\0\0\0\0\0\0\0\0" # MEMBOT (30 bytes)
- b"\0\0" # NMIADD
- b"\0\0" # RAMTOP (patched later)
- b"\xff\xff", # P_RAMT
- )
-
- ChansData = (
- # CHANS data (routine pointers, channel name)
- b"\xf4\x09\xa8\x10"
- b"K" # PRINT_OUT, KEY_INPUT, "K"
- b"\xf4\x09\xc4\x15"
- b"S" # PRINT_OUT, REPORT_J, "S"
- b"\x81\x0f\xc4\x15"
- b"R" # ADD_CHAR, REPORT_J, "R"
- b"\xf4\x09\xc4\x15"
- b"P" # PRINT_OUT, REPORT_J, "P"
- b"\x80" # Terminator
- )
+ sna_data.extend(snapshot.mem)
+ sna_data[SP - 16384 + 0 + snaHeaderLen] = snapshot.PCL
+ sna_data[SP - 16384 + 1 + snaHeaderLen] = snapshot.PCH
- # CHANS data starts at 23734 and could vary in length depending on
- # the presence of an Interface 1.
- self.patchAddr(23734, ChansData)
-
- # BASIC start (usually 23755 in absence of Interface 1)
- self.BasicStart = 23734 + len(ChansData)
+ return sna_data
def emit(
self,
@@ -188,79 +116,8 @@ def emit(
):
"""Emit a .SNA file with the compiled bytes; ignores loader_bytes"""
- # Clear address could be different from entry_point-1 in future.
- clear_addr = entry_point - 1
-
- # Ignore loader_bytes and use our own BASIC program
- loader_bytes = bytearray(
- b"\0\x0a" # BASIC big endian line num
- b"\x0f\0" # BASIC little endian line length
- b"\xfa\xc0" # BASIC IF USR
- )
- loader_bytes.extend(b"%05d\x0e\0\0\0\0\0" % entry_point)
- loader_bytes[-3:-1] = self.word(entry_point)
- loader_bytes.extend(b"\xcb\x0d") # THEN + final newline
- BasicLength = len(loader_bytes)
-
- # Address to array index conversion offset; 0x1B is the header size
- BasicEnd = self.BasicStart + BasicLength
-
- # Clear everything from the channel variables to the UDG start
- self.patchAddr(self.BasicStart, b"\x00" * (65368 - self.BasicStart))
-
- # Patch SP register in header
- self.patchIdx(0x17, self.word(clear_addr - 5))
-
- # Patch ERR_SP
- self.patchAddr(23613, self.word(clear_addr - 3))
- # Patch VARS
- self.patchAddr(23627, self.word(BasicEnd))
- # Patch E_LINE
- self.patchAddr(23641, self.word(BasicEnd + 1))
- # Patch WORKSP
- self.patchAddr(23649, self.word(BasicEnd + 4))
- # Patch STKBOT
- self.patchAddr(23651, self.word(BasicEnd + 4))
- # Patch STKEND
- self.patchAddr(23653, self.word(BasicEnd + 4))
- # Patch RAMTOP
- self.patchAddr(23730, self.word(clear_addr))
-
- # Patch BASIC program
- self.patchAddr(self.BasicStart, loader_bytes)
-
- # Patch variables, edit line and calculator stack (edit line contains
- # a RUN command, 0xF7; calculator stack is empty)
- self.patchAddr(BasicEnd, b"\x80\xf7\x0d\x80")
-
- # Patch stack
- self.patchAddr(
- clear_addr - 5,
- b"\x9e\x1b" # Entry address: LINE_NEW
- b"\x03\x13" # Error resume routine (ERR_SP points here): MAIN_4
- b"\x00\x3e", # GOSUB stack end marker
- )
-
- # UDG area (might be overwritten by compiled code)
- self.patchAddr(
- 65368,
- b"\x00\x3c\x42\x42\x7e\x42\x42\x00\x00\x7c\x42\x7c\x42\x42\x7c\x00"
- b"\x00\x3c\x42\x40\x40\x42\x3c\x00\x00\x78\x44\x42\x42\x44\x78\x00"
- b"\x00\x7e\x40\x7c\x40\x40\x7e\x00\x00\x7e\x40\x7c\x40\x40\x40\x00"
- b"\x00\x3c\x42\x40\x4e\x42\x3c\x00\x00\x42\x42\x7e\x42\x42\x42\x00"
- b"\x00\x3e\x08\x08\x08\x08\x3e\x00\x00\x02\x02\x02\x42\x42\x3c\x00"
- b"\x00\x44\x48\x70\x48\x44\x42\x00\x00\x40\x40\x40\x40\x40\x7e\x00"
- b"\x00\x42\x66\x5a\x42\x42\x42\x00\x00\x42\x62\x52\x4a\x46\x42\x00"
- b"\x00\x3c\x42\x42\x42\x42\x3c\x00\x00\x7c\x42\x42\x7c\x40\x40\x00"
- b"\x00\x3c\x42\x42\x52\x4a\x3c\x00\x00\x7c\x42\x42\x7c\x44\x42\x00"
- b"\x00\x3c\x40\x3c\x02\x42\x3c\x00\x00\xfe\x10\x10\x10\x10\x10\x00"
- b"\x00\x42\x42\x42\x42\x42\x3c\x00",
- )
-
- # Patch compiled code in
- assert entry_point + len(program_bytes) <= 65536
- self.patchAddr(entry_point, program_bytes)
+ sna_data = self.generate(None, entry_point - 1, entry_point, program_bytes)
# Write output file
with open(output_filename, "wb") as f:
- f.write(self.output)
+ f.write(sna_data)
diff --git a/src/outfmt/z80.py b/src/outfmt/z80.py
new file mode 100644
index 000000000..7298dc40a
--- /dev/null
+++ b/src/outfmt/z80.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+
+# 48K .Z80 format output module
+#
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file AUTHORS for copyright details.
+#
+# This file is part of Boriel BASIC Compiler.
+#
+# Boriel BASIC Compiler is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# Boriel BASIC Compiler is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Boriel BASIC Compiler. If not, see .
+
+
+import io
+
+from .codeemitter import CodeEmitter
+from .gensnapshot import GenSnapshot
+
+
+class Z80Emitter(CodeEmitter):
+ """Class to emit 48K .Z80 snapshots"""
+
+ def __init__(self):
+ """Initializes the .Z80 generation"""
+
+ super().__init__()
+
+ def generate(
+ self,
+ loader_bytes,
+ clear_addr,
+ entry_point,
+ program_bytes,
+ ):
+ """Generate the bytes of a .Z80 format snapshot"""
+
+ """
+ Format of .Z80 version 1 (the only one we generate here):
+
+ Offset Length Description
+ ---------------------------
+ 0 1 A register
+ 1 1 F register
+ 2 2 BC register pair (LSB, i.e. C, first)
+ 4 2 HL register pair
+ 6 2 Program counter
+ 8 2 Stack pointer
+ 10 1 Interrupt register
+ 11 1 Refresh register (Bit 7 is not significant!)
+ 12 1 Bit 0 : Bit 7 of the R-register
+ Bit 1-3: Border colour
+ Bit 4 : 1=Basic SamRom switched in
+ Bit 5 : 1=Block of data is compressed
+ Bit 6-7: No meaning
+ 13 2 DE register pair
+ 15 2 BC' register pair
+ 17 2 DE' register pair
+ 19 2 HL' register pair
+ 21 1 A' register
+ 22 1 F' register
+ 23 2 IY register (Again LSB first)
+ 25 2 IX register
+ 27 1 Interrupt flipflop, 0=DI, otherwise EI
+ 28 1 IFF2 (not particularly important...)
+ 29 1 Bit 0-1: Interrupt mode (0, 1 or 2)
+ Bit 2 : 1=Issue 2 emulation
+ Bit 3 : 1=Double interrupt frequency
+ Bit 4-5: 1=High video synchronisation
+ 3=Low video synchronisation
+ 0,2=Normal
+ Bit 6-7: 0=Cursor/Protek/AGF joystick
+ 1=Kempston joystick
+ 2=Sinclair 2 Left joystick (or user
+ defined, for version 3 .z80 files)
+ 3=Sinclair 2 Right joystick
+
+ After that, a run-length compressed stream follows, containing the
+ 48 KB of the memory. Each block of 5 to 255 bytes that are all equal
+ (or 2 to 255 if the repeated bytes are ED) is replaced with ED ED xx yy,
+ where xx is the repeat count and yy the byte to repeat. The byte
+ following a single ED is not considered eligible as a block.
+
+ After the memory dump, an end marker comes: 00 ED ED 00
+
+ Examples:
+ 01 01 01 01 01 01 01 -> ED ED 07 01
+ ED ED 22 22 -> ED ED 02 ED 22 22
+ (the first two ED's must be run-length encoded, otherwise ambiguity
+ would arise).
+ ED 00 00 00 00 00 00 -> ED 00 ED ED 05 00
+ (only the bytes starting after the second 00 are considered for
+ compression; the first ED 00 sequence is encoded verbatim).
+
+ """
+
+ snapshot = GenSnapshot(loader_bytes, clear_addr, entry_point, program_bytes)
+
+ z80 = io.BytesIO()
+
+ z80.write(
+ bytes(
+ (
+ snapshot.A,
+ snapshot.F,
+ snapshot.C,
+ snapshot.B,
+ snapshot.L,
+ snapshot.H,
+ snapshot.PCL,
+ snapshot.PCH,
+ snapshot.SPL,
+ snapshot.SPH,
+ snapshot.I,
+ snapshot.R & 0x7F,
+ ((snapshot.R & 0x80) >> 7) | ((snapshot.outFE & 7) << 1) | 0x20,
+ snapshot.E,
+ snapshot.D,
+ snapshot.C2,
+ snapshot.B2,
+ snapshot.E2,
+ snapshot.D2,
+ snapshot.L2,
+ snapshot.H2,
+ snapshot.A2,
+ snapshot.F2,
+ snapshot.IYL,
+ snapshot.IYH,
+ snapshot.IXL,
+ snapshot.IXH,
+ 1 if snapshot.IFF1 else 0, # IFF1
+ 1 if snapshot.IFF2 else 0, # IFF2
+ snapshot.IM & 3,
+ )
+ )
+ )
+
+ idx: int = 0
+ runlength: int = 0
+ b: int = -1
+
+ while True:
+ if idx == 49152:
+ break
+
+ b = snapshot.mem[idx]
+ idx += 1
+ if idx != 49152 and b == snapshot.mem[idx]:
+ # Repetition found
+ runlength = 1
+
+ # Find the end of this run
+ while idx != 49152 and runlength != 255:
+ if b != snapshot.mem[idx]:
+ break
+ idx += 1
+ runlength += 1
+
+ if runlength < 5 and b != 0xED:
+ # Doesn't qualify for compression
+ z80.write(bytes((b,)) * runlength)
+ else:
+ # Must compress
+ z80.write(bytes((0xED, 0xED, runlength, b)))
+
+ else:
+ z80.write(bytes((b,)))
+ # Store byte after ED and don't consider it for run length
+ if b == 0xED and idx != 49152:
+ z80.write(bytes((snapshot.mem[idx],)))
+ idx += 1
+
+ z80.write(b"\0\xed\xed\0")
+ return z80.getvalue()
+
+ def emit(
+ self,
+ output_filename,
+ program_name,
+ loader_bytes,
+ entry_point,
+ program_bytes,
+ aux_bin_blocks,
+ aux_headless_bin_blocks,
+ ):
+ """Save a .Z80 file with the compiled bytes; ignores loader_bytes"""
+
+ output = self.generate(None, entry_point - 1, entry_point, program_bytes)
+
+ # Write output file
+ with open(output_filename, "wb") as f:
+ f.write(output)
diff --git a/src/zxbasm/asmparse.py b/src/zxbasm/asmparse.py
index 08115b628..3c4e9b92e 100755
--- a/src/zxbasm/asmparse.py
+++ b/src/zxbasm/asmparse.py
@@ -1016,7 +1016,7 @@ def assemble(input_):
def generate_binary(outputfname, format_, progname="", binary_files=None, headless_binary_files=None, emitter=None):
"""Outputs the memory binary to the
output filename using one of the given
- formats: tap, tzx, sna or bin
+ formats: tap, tzx, sna, z80 or bin
"""
global AUTORUN_ADDR
@@ -1066,6 +1066,8 @@ def generate_binary(outputfname, format_, progname="", binary_files=None, headle
emitter = {"tap": outfmt.TAP, "tzx": outfmt.TZX}[format_]()
elif format_ == "sna":
emitter = outfmt.SnaEmitter()
+ elif format_ == "z80":
+ emitter = outfmt.Z80Emitter()
else:
emitter = outfmt.BinaryEmitter()
diff --git a/src/zxbc/args_config.py b/src/zxbc/args_config.py
index 58bb423fb..c3ae2e59c 100644
--- a/src/zxbc/args_config.py
+++ b/src/zxbc/args_config.py
@@ -120,11 +120,15 @@ def parse_options(args: list[str] | None = None) -> Namespace:
FileType.TAP,
FileType.TZX,
FileType.SNA,
+ FileType.Z80,
}:
- parser.error("Options --BASIC and --autorun require one of tzx, tap, or sna output format")
+ parser.error("Options --BASIC and --autorun require one of sna, tzx, tap or z80 output format")
- if not (options.basic and options.autorun) and OPTIONS.output_file_type == FileType.SNA:
- parser.error("Options --BASIC and --autorun are both required for sna format")
+ if not (options.basic and options.autorun) and OPTIONS.output_file_type in {
+ FileType.SNA,
+ FileType.Z80,
+ }:
+ parser.error("Options --BASIC and --autorun are both required for snapshot formats")
if options.append_binary and OPTIONS.output_file_type not in {FileType.TAP, FileType.TZX}:
parser.error("Option --append-binary needs either tap or tzx output format")
diff --git a/src/zxbc/args_parser.py b/src/zxbc/args_parser.py
index 57358667b..a28e32d82 100644
--- a/src/zxbc/args_parser.py
+++ b/src/zxbc/args_parser.py
@@ -17,6 +17,7 @@ class FileType(StrEnum):
SNA = "sna"
TAP = "tap"
TZX = "tzx"
+ Z80 = "z80"
def parse_warning_option(code: str) -> str:
diff --git a/tests/functional/Makefile b/tests/functional/Makefile
index 6fc35ceda..55a0ed282 100644
--- a/tests/functional/Makefile
+++ b/tests/functional/Makefile
@@ -27,7 +27,7 @@ basic_tests:
./test.py '**/[0-9]*.bas'
bin:
- ./test.py '**/tzx_*.bas' '**/tap_*.bas' '**/sna_*.bas'
+ ./test.py '**/tzx_*.bas' '**/tap_*.bas' '**/sna_*.bas' '**/z80_*.bas'
# Measures coverage using only basic tests
.PHONY: basic_coverage
diff --git a/tests/functional/arch/zx48k/z80_00.bas b/tests/functional/arch/zx48k/z80_00.bas
new file mode 100644
index 000000000..06ceceac4
--- /dev/null
+++ b/tests/functional/arch/zx48k/z80_00.bas
@@ -0,0 +1 @@
+PRINT "HELLO WORLD"
diff --git a/tests/functional/arch/zx48k/z80_00.z80 b/tests/functional/arch/zx48k/z80_00.z80
new file mode 100644
index 000000000..a614187a4
Binary files /dev/null and b/tests/functional/arch/zx48k/z80_00.z80 differ
diff --git a/tests/functional/cmdline/test_cmdline.txt b/tests/functional/cmdline/test_cmdline.txt
index ac3246a65..8be8c00db 100644
--- a/tests/functional/cmdline/test_cmdline.txt
+++ b/tests/functional/cmdline/test_cmdline.txt
@@ -4,8 +4,8 @@
>>> process_file('arch/zx48k/arrbase1.bas', ['-q', '-S', '-O --mmap arrbase1.map'])
usage: zxbc.py [-h] [-d] [-O OPTIMIZE] [-o OUTPUT_FILE] [-T] [-t] [-A] [-E]
- [--parse-only] [-f {asm,bin,IR,sna,tap,tzx}] [-B] [-a] [-S ORG]
- [-e STDERR] [--array-base ARRAY_BASE]
+ [--parse-only] [-f {asm,bin,IR,sna,tap,tzx,z80}] [-B] [-a]
+ [-S ORG] [-e STDERR] [--array-base ARRAY_BASE]
[--string-base STRING_BASE] [-Z] [-H HEAP_SIZE]
[--heap-address HEAP_ADDRESS] [--debug-memory] [--debug-array]
[--strict-bool] [--enable-break] [--explicit] [-D DEFINES]
diff --git a/tests/functional/test.py b/tests/functional/test.py
index d28bceedf..6d5f4eefa 100755
--- a/tests/functional/test.py
+++ b/tests/functional/test.py
@@ -11,12 +11,13 @@
import sys
import tempfile
from collections.abc import Callable, Iterable
-from typing import Final
+from typing import Final, cast
+from src.api.utils import open_file
from src.zxbc.args_parser import FileType
reOPT = re.compile(r"^opt([0-9]+)_") # To detect -On tests
-reBIN = re.compile(r"^(?:.*/)?(tzx|tap|sna)_.*") # To detect tzx / tap / sna test
+reBIN = re.compile(r"^(?:.*/)?(tzx|tap|sna|z80)_.*") # To detect tzx / tap / snapshot test
reIC = re.compile(r"^.*_IC$") # To detect intermediate code tests
EXIT_CODE = 0
@@ -124,10 +125,8 @@ def get_file_lines(
"""Opens source file and load its lines,
discarding those not important for comparison.
"""
- from src.api.utils import open_file
-
with open_file(filename, "rt", "utf-8") as f:
- lines = [x.rstrip("\r\n") for x in f]
+ lines = [cast(str, x.rstrip("\r\n")) for x in f]
if ignore_regexp is not None:
r = re.compile(ignore_regexp)
@@ -240,7 +239,7 @@ def _get_testbas_options(fname: str) -> tuple[list[str], str, str]:
match_bin = reBIN.match(getName(fname))
match_ic = reIC.match(getName(fname))
- if match_bin and match_bin.groups()[0].lower() in (FileType.SNA, FileType.TAP, FileType.TZX):
+ if match_bin and match_bin.groups()[0].lower() in {FileType.SNA, FileType.TAP, FileType.TZX, FileType.Z80}:
ext = match_bin.groups()[0].lower()
tfname = os.path.join(TEMP_DIR, getName(fname) + os.extsep + ext)
options.extend([f"--output-format={ext}", fname, "-o", tfname, "-a", "-B"] + prep)
diff --git a/tests/runtime/test_case b/tests/runtime/test_case
index cdfac9e9b..5ca2603f9 100755
--- a/tests/runtime/test_case
+++ b/tests/runtime/test_case
@@ -8,13 +8,10 @@ TIMEOUT=180
TIMEKILL=$((TIMEOUT+30))
echo -n "Testing $(basename $1): "
-SNA=$(basename -s .bas $1).sna
RUN=$(basename -s .bas $1).z80
EXPECTED=$(basename -s .bas $1).tzx.scr
-rm -f "$RUN" 2>/dev/null
-../../zxbc.py --sna -aB -o "$SNA" $1 $(grep "PARAMS:" $1 |cut -d':' -f2-) --debug-memory 2>/dev/null
-snapconv "$SNA" "$RUN" 2>/dev/null 1>/dev/null
+../../zxbc.py -f z80 -aB -o "$RUN" $1 $(grep "PARAMS:" $1 |cut -d':' -f2-) --debug-memory 2>/dev/null
timeout -k $TIMEKILL $TIMEOUT ./check_test.py "$RUN" "./expected/${EXPECTED}"
RETVAL=$?
-rm -f "$RUN" "$SNA" 2>/dev/null
+rm -f "$RUN" 2>/dev/null
exit $RETVAL
diff --git a/tests/runtime/update_test.sh b/tests/runtime/update_test.sh
index bb4af8396..8a67c152b 100755
--- a/tests/runtime/update_test.sh
+++ b/tests/runtime/update_test.sh
@@ -2,9 +2,6 @@
# ./run $1
NAME=$(basename -s .bas $1).z80
-RUN=$(basename -s .bas $1)
-rm -f "$NAME"
-../../zxbc.py --sna -aB "$@" --debug-memory || exit 1
-snapconv "$RUN.sna" "$NAME"
+../../zxbc.py -f z80 -aB "$@" --debug-memory || exit 1
./update_test.py $NAME
# mv $NAME.scr expected