diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df443c --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +.idea +*.log +tmp/ + +*.py[cod] +*.egg +build +htmlcov +src/gbasm.egg-info +**/.mypy_cache +**/.pytest_cache +**/.tox +**/__pycache__ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..8d68fa0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,59 @@ +# Change pip's cache directory to be inside the project directory since we can +# only cache local items. +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +# Pip's cache doesn't store the python packages +# https://pip.pypa.io/en/stable/reference/pip_install/#caching +# +# If you want to also cache the installed packages, you have to install +# them in a virtualenv and cache it as well. +cache: + paths: + - .cache/pip + - venv/ + +stages: + - lint + - test + +before_script: + - python -V + - pip install virtualenv + - virtualenv venv + - source venv/bin/activate + +lint: + stage: lint + image: python:3 + script: + - pip3 install black + - black --check src/ + +python-37: + stage: test + image: python:3.7 + script: + - pip3 install tox + - tox -e py37 + +python-38: + stage: test + image: python:3.8 + script: + - pip3 install tox + - tox -e py38 + +python-39: + stage: test + image: python:3.9 + script: + - pip3 install tox + - tox -e py39 + +python-310: + stage: test + image: python:3.10 + script: + - pip3 install tox + - tox -e py310 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..aa2f8c5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,36 @@ +[metadata] +name = gbasm +version = 0.0.1 +author = Max Regan +author_email = mgregan2@gmail.com +description = An assembler for Gameboy assembly language +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.7 +tests_require = pytest +test_suite = test_assemble +requires = pyyaml, foobarbdfasf + +[options.packages.find] +where = + src + test + +[options.entry_points] +console_scripts = + gbasm = gbasm.gbasm:main + +[tox:tox] +envlist = py3{7,8,9,10} + +[testenv] +deps = pytest + pyyaml +commands = pytest diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a2fc30f --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +import os +from setuptools import setup + +setup() diff --git a/src/gbasm/__init__.py b/src/gbasm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gbasm/arguments/ArgumentParser.py b/src/gbasm/arguments/ArgumentParser.py new file mode 100644 index 0000000..8b63771 --- /dev/null +++ b/src/gbasm/arguments/ArgumentParser.py @@ -0,0 +1,6 @@ +class ArgumentParser(object): + def can_parse(token: str) -> bool: + raise NotImplementedError() + + def get_name() -> str: + raise NotImplementedError() diff --git a/src/gbasm/arguments/ArgumentParsers.py b/src/gbasm/arguments/ArgumentParsers.py new file mode 100644 index 0000000..368bf49 --- /dev/null +++ b/src/gbasm/arguments/ArgumentParsers.py @@ -0,0 +1,141 @@ +from .ArgumentParser import ArgumentParser +from . import Arguments + + +class Address(ArgumentParser): + + NAME = "Address" + + def __init__(self): + pass + + def can_parse(self, token: str) -> bool: + try: + addr = int(token, base=0) + return addr < 0x8000 and addr >= 0 + except ValueError: + return False + + def get_name(self) -> str: + return Address.NAME + + +class Label(ArgumentParser): + + NAME = "Label" + + def __init__(self): + pass + + def can_parse(self, token: str) -> bool: + if not token[0] in string.ascii_letters + ["_"]: + return False + return True + + def parse(self, token) -> Arguments.Register8: + return Arguments.Label(token) + + def get_name(self) -> str: + return Label.NAME + + +class Register8(ArgumentParser): + + NAME = "Register8" + + def __init__(self, indirect: bool = False, indirect_increment: bool = False): + self.indirect = indirect + + def can_parse(self, token: str) -> bool: + if token in Arguments.Register8.REGISTERS: + return True + if self.indirect and token in Arguments.Register8.REGISTERS_INDIRECT: + return True + if ( + self.indirect_increment + and token in Arguments.Register8.REGISTERS_INDIRECT_INCREMENT + ): + return True + return False + + def parse(self, token) -> Arguments.Register8: + return Arguments.Register8(token) + + def get_name(self) -> str: + return Register8.NAME + + +class Register16(ArgumentParser): + + NAME = "Immediate8" + + def __init__(self, indirect: bool = False, indirect_increment: bool = False): + self.indirect = indirect + + def can_parse(self, token: str) -> bool: + return token in Arguments.Register16.REGISTERS + + def to_argument(self, token: str): + return None + + def parse(self, token) -> Arguments.Register16: + return Arguments.Register16(token) + + def get_name(self) -> str: + return Register16.NAME + + +class Immediate8(ArgumentParser): + + NAME = "Immediate8" + + def __init__(self): + pass + + def can_parse(self, token: str) -> bool: + try: + addr = int(token, base=0) + return addr <= 0xFF and addr >= 0 + except ValueError: + return False + + def get_name(self) -> str: + return Immediate8.NAME + + +class Immediate16(ArgumentParser): + + NAME = "Immediate16" + + def __init__(self): + pass + + def can_parse(self, token: str) -> bool: + try: + addr = int(token, base=0) + return addr <= 0xFFFF and addr >= 0 + except ValueError: + return False + + def parse(self, token) -> Arguments.Immediate16: + return Arguments.Immediate16(token) + + def get_name(self) -> str: + return Immediate16.NAME + + +class Flag(ArgumentParser): + + NAME = "Flag" + + def __init__(self): + pass + + def can_parse(self, token: str) -> bool: + return token in Arguments.Flag.FLAGS + + def parse(self, token) -> Arguments.Immediate16: + return Arguments.Flag(token) + + def get_name(self) -> str: + return Flag.NAME diff --git a/src/gbasm/arguments/Arguments.py b/src/gbasm/arguments/Arguments.py new file mode 100644 index 0000000..0df463f --- /dev/null +++ b/src/gbasm/arguments/Arguments.py @@ -0,0 +1,72 @@ +class Argument(object): + def __init__(self): + pass + + +class Address(Argument): + + NAME = "Address" + + def __init__(self, value: int): + self.value = value + + +class Label(Argument): + + NAME = "Label" + + def __init__(self, value: str): + self.value = value + + +class Register8(Argument): + + NAME = "Register8" + REGISTERS = ["A", "B", "C", "D", "E", "H", "L"] + REGISTERS_INDIRECT = ["(HL)"] + REGISTERS_INDIRECT_INCREMENT = ["(HL)", "(HL+)", "(HL-)"] + + def __init__(self, value: str): + if value not in Register8.REGISTERS + Register8.REGISTERS_INDIRECT_INCREMENT: + raise ValueError("Unknown Register8: {}".format(value)) + self.value = value + + +class Register16(Argument): + + NAME = "Immediate8" + + REGISTERS = ["BC", "DE", "DE", "HL", "SP"] + + def __init__(self, value: int): + if value not in Register16.REGISTERS: + raise ValueError("Unknown Register16: {}".format(value)) + self.value = value + + +class Immediate8(Argument): + + NAME = "Immediate8" + + def __init__(self, value: int): + self.value = value + + +class Immediate16(Argument): + + NAME = "Immediate16" + + def __init__(self, value: int): + self.value = value + + +class Flag(Argument): + + NAME = "Immediate8" + + FLAGS = ["Z", "C", "NZ", "NC"] + + def __init__(self, value: str): + if value not in Flag.FLAGS: + raise ValueError("Unknown Flag: {}".format(value)) + self.value = value diff --git a/src/gbasm/arguments/__init__.py b/src/gbasm/arguments/__init__.py new file mode 100644 index 0000000..a5c9eaf --- /dev/null +++ b/src/gbasm/arguments/__init__.py @@ -0,0 +1,10 @@ +from .ArgumentParser import ArgumentParser +from .ArgumentParsers import ( + Label, + Address, + Immediate8, + Immediate16, + Register8, + Register16, +) +from .Arguments import Argument diff --git a/src/gbasm/gbasm.py b/src/gbasm/gbasm.py new file mode 100755 index 0000000..17bec8c --- /dev/null +++ b/src/gbasm/gbasm.py @@ -0,0 +1,178 @@ +#!/usr/bin/python3 + +import argparse +import logging +import sys + +from .instructions import Instruction +from .instructions.inc import Inc +from .instructions.dec import Dec +from .instructions.nop import Nop +from .instructions.stop import Stop +from .instructions.jr import Jr +from .instructions.ld import Ld +from .arguments import ArgumentParser, Argument + +from typing import Callable, Dict, List, Optional + +logger = logging.getLogger(__name__) + +COMMENT_CHAR = "#" +LABEL_SUFFIX = ":" + +GB_INSTRUCTIONS = [ + Nop(), + Stop(), + Inc(), + Dec(), + Jr(), + Ld(), +] + + +def build_instruction_map() -> Dict[str, Instruction]: + d = {} # type: Dict[str, Instruction] + for i in GB_INSTRUCTIONS: + d[i.token] = i + return d + + +def try_parse_arguments( + args: List[str], arg_types: List[ArgumentParser] +) -> Optional[List[Argument]]: + if len(args) != len(arg_types): + return None + + out_args = [] + + for (arg, arg_type) in zip(args, arg_types): + try: + out_args.append(arg_type.parse(arg)) + except ValueError: + return None + + return out_args + + +def parse_line_size(instruction: Instruction, arguments: List[str]) -> bytes: + + for argtype_list in instruction.argument_specs: + args = try_parse_arguments(arguments, argtype_list) + if args is not None: + return instruction.num_bytes(args) + + raise ValueError("Failed to parse line.") + + +def parse_line_bytes( + instruction: Instruction, + arguments: List[str], + instruction_addr: int, + label_resolver: Callable[[str], int], +) -> bytes: + + for argtype_list in instruction.argument_specs: + args = try_parse_arguments(arguments, argtype_list) + if args is not None: + return instruction.to_bytes(args, instruction_addr, label_resolver) + + raise ValueError("Failed to parse line.") + + +def assemble_file(infile) -> bytes: + program = infile.readlines() + return assemble(program) + + +def assemble(lines: str) -> bytes: + + instruction_map = build_instruction_map() + logger.debug("Instruction map: %s", instruction_map) + + labels = {} # type: Dict[str, int] + program = bytes() + + def label_resolver(label: str) -> int: + nonlocal labels + return labels[label] + + for step in ["SIZE", "CONTENT"]: + logger.debug("Starting step: %s", step) + byte_offset = 0 + + for line_num, line in enumerate(lines): + # Remove comments + line = line.split(COMMENT_CHAR)[0] + + # Tokenize + tokens = line.split() + + if len(tokens) == 0: + continue + + instruction_name = tokens[0] + args = tokens[1:] + try: + instruction = instruction_map[instruction_name] + except KeyError: + if instruction_name[-1] == LABEL_SUFFIX: + if step == "SIZE": + label = instruction_name[:-1] + logger.debug("Found label '%s' at %s", label, byte_offset) + if label in labels.keys(): + raise KeyError( + "Label '%s' defined at %s and %s", + label, + labels[label], + line_num, + ) + labels[label] = byte_offset + continue + raise KeyError( + 'Unknown instruction "%s" on line %s', instruction_name, line_num + ) + + if step == "CONTENT": + try: + program += parse_line_bytes( + instruction, args, byte_offset, label_resolver + ) + except ValueError: + raise ValueError("Failed to parse line %s,\n%s", line_num, line) + + byte_offset += parse_line_size(instruction, args) + if step == "SIZE": + logger.info("Program size: %s bytes", byte_offset) + logger.debug("Found labels: %s", labels) + + return program + + +def main() -> None: + parser = argparse.ArgumentParser(description="An assembler for Gameboy assembly") + + parser.add_argument( + "--infile", "-i", type=argparse.FileType("r"), default=sys.stdin + ) + parser.add_argument( + "--outfile", "-o", type=argparse.FileType("wb", 0), default=sys.stdout + ) + parser.add_argument("--verbose", "-v", action="store_true") + + args = parser.parse_args() + + logging.basicConfig(format="%(levelname)s: %(message)s") + logger.setLevel(logging.INFO) + + if args.verbose: + logging.basicConfig( + format="%(levelname)s: %(filename)s:%(lineno)d: %(message)s" + ) + logger.setLevel(logging.DEBUG) + + program = assemble_file(args.infile) + args.outfile.write(program) + + +if __name__ == "__main__": + main() diff --git a/src/gbasm/instructions/Instruction.py b/src/gbasm/instructions/Instruction.py new file mode 100644 index 0000000..bd120a8 --- /dev/null +++ b/src/gbasm/instructions/Instruction.py @@ -0,0 +1,19 @@ +from typing import Callable, List +from ..arguments import Argument, ArgumentParser + + +class Instruction(object): + def __init__(self, token: str, argument_specs: List[List[ArgumentParser]]): + self.token = token + self.argument_specs = argument_specs + + def num_bytes(self, arguments) -> int: + raise NotImplementedError() + + def to_bytes( + self, + arguments: List[Argument], + instruction_addr: int, + label_resolver: Callable[[str], int], + ) -> bytes: + raise NotImplementedError() diff --git a/src/gbasm/instructions/__init__.py b/src/gbasm/instructions/__init__.py new file mode 100755 index 0000000..1c18381 --- /dev/null +++ b/src/gbasm/instructions/__init__.py @@ -0,0 +1 @@ +from .Instruction import Instruction diff --git a/src/gbasm/instructions/dec.py b/src/gbasm/instructions/dec.py new file mode 100644 index 0000000..7ced504 --- /dev/null +++ b/src/gbasm/instructions/dec.py @@ -0,0 +1,52 @@ +from .Instruction import Instruction +from ..arguments.ArgumentParsers import Register8, Register16 +from ..arguments import Argument +from typing import Callable, List + + +class Dec(Instruction): + def __init__(self): + argtypes = [[Register8()], [Register16()]] + super().__init__("DEC", argtypes) + + def num_bytes(self, arguments) -> int: + return 1 + + def to_bytes( + self, + arguments: List[Argument], + instruction_addr: int, + label_resolver: Callable[[str], int], + ) -> bytes: + + if len(arguments) != 1: + raise ValueError("Incorrect number of arguments") + + value = arguments[0].value + + if value == "BC": + return bytes([0x0B]) + if value == "DE": + return bytes([0x1B]) + if value == "HL": + return bytes([0x2B]) + if value == "SP": + return bytes([0x3B]) + if value == "A": + return bytes([0x3D]) + if value == "B": + return bytes([0x05]) + if value == "C": + return bytes([0x0D]) + if value == "D": + return bytes([0x15]) + if value == "E": + return bytes([0x1D]) + if value == "H": + return bytes([0x25]) + if value == "L": + return bytes([0x2D]) + if value == "(HL)": + return bytes([0x35]) + + raise ValueError("Unknown value: {}".format(value)) diff --git a/src/gbasm/instructions/inc.py b/src/gbasm/instructions/inc.py new file mode 100644 index 0000000..88df079 --- /dev/null +++ b/src/gbasm/instructions/inc.py @@ -0,0 +1,52 @@ +from .Instruction import Instruction +from ..arguments.ArgumentParsers import Register8, Register16 +from ..arguments import Argument +from typing import Callable, List + + +class Inc(Instruction): + def __init__(self): + argtypes = [[Register8()], [Register16()]] + super().__init__("INC", argtypes) + + def num_bytes(self, arguments) -> int: + return 1 + + def to_bytes( + self, + arguments: List[Argument], + instruction_addr: int, + label_resolver: Callable[[str], int], + ) -> bytes: + + if len(arguments) != 1: + raise ValueError("Incorrect number of arguments") + + value = arguments[0].value + + if value == "BC": + return bytes([0x03]) + if value == "DE": + return bytes([0x13]) + if value == "HL": + return bytes([0x23]) + if value == "SP": + return bytes([0x33]) + if value == "A": + return bytes([0x3C]) + if value == "B": + return bytes([0x04]) + if value == "C": + return bytes([0x0C]) + if value == "D": + return bytes([0x14]) + if value == "E": + return bytes([0x1C]) + if value == "H": + return bytes([0x24]) + if value == "L": + return bytes([0x2C]) + if value == "(HL)": + return bytes([0x34]) + + raise ValueError("Unknown value: {}".format(value)) diff --git a/src/gbasm/instructions/jr.py b/src/gbasm/instructions/jr.py new file mode 100644 index 0000000..0cc6073 --- /dev/null +++ b/src/gbasm/instructions/jr.py @@ -0,0 +1,60 @@ +from .Instruction import Instruction +from ..arguments.ArgumentParsers import Flag, Label +from ..arguments import Argument +from typing import Callable, List + + +class Jr(Instruction): + + INSTRUCTION_SIZE = 2 + + def __init__(self): + argtypes = [[Label()], [Flag(), Label()]] + super().__init__("JR", argtypes) + + def num_bytes(self, arguments) -> int: + return Jr.INSTRUCTION_SIZE + + def to_bytes( + self, + arguments: List[Argument], + instruction_address: int, + label_resolver: Callable[[str], int], + ) -> bytes: + + out_bytes = bytearray() + + if len(arguments) == 1: + out_bytes.append(0x18) + dest_arg = arguments[0].value + + elif len(arguments) == 2: + flag_dict = {"NZ": 0x20, "NC": 0x30, "Z": 0x28, "C": 0x38} + flag = arguments[0].value + try: + out_bytes.append(flag_dict[flag]) + except KeyError: + logger.exception("Instruction JR does not accept flag %s", arg) + dest_arg = arguments[1].value + + else: + raise ValueError("Incorrect number of arguments") + + if isinstance(dest_arg, str): + label = dest_arg + dest_addr = label_resolver(label) + else: + dest_addr = dest_arg + + addr_offset = dest_addr - (instruction_address + Jr.INSTRUCTION_SIZE) + if addr_offset > 127 or addr_offset < -128: + raise ValueError( + "JR instruction cannot jump to {} ({}), from {}, {} bytes away".format( + label, dest_addr, instruction_address, addr_offset + ) + ) + if addr_offset < 0: + addr_offset += 256 + + out_bytes.append(addr_offset) + return bytes(out_bytes) diff --git a/src/gbasm/instructions/ld.py b/src/gbasm/instructions/ld.py new file mode 100644 index 0000000..87f27af --- /dev/null +++ b/src/gbasm/instructions/ld.py @@ -0,0 +1,40 @@ +from .Instruction import Instruction +from ..arguments.ArgumentParsers import Register16, Immediate16 +from ..arguments import Argument +from typing import Callable, List + + +class Ld(Instruction): + def __init__(self): + argtypes = [[Register16(), Immediate16()]] + super().__init__("LD", argtypes) + + def num_bytes(self, arguments) -> int: + # the instruction_address and label_resolver are unused + return len(self.to_bytes(arguments, 0, lambda x: None)) + + def encode_reg_16(self, register: Register16, immediate: Immediate16) -> bytes: + reg_dict = {"BC": 0x01, "DE": 0x11, "HL": 0x21, "SP": 0x31} + imm = int(immediate.value, 0) + print("immediate", imm) + out = bytearray() + out.append(reg_dict[register.value]) + out.extend(imm.to_bytes(2, "little", signed=imm < 0)) + return bytes(out) + + def to_bytes( + self, + arguments: List[Argument], + instruction_address: int, + label_resolver: Callable[[str], int], + ) -> bytes: + + # print(arguments) + # print("Register16", type(arguments[0])) + # print("Immediate16", isinstance(arguments[1], Immediate16)) + + # if isinstance(arguments[0], Register16) and \ + # isinstance(arguments[1], Immediate16): + return self.encode_reg_16(arguments[0], arguments[1]) + + raise TypeError("Unhandled argument types") diff --git a/src/gbasm/instructions/nop.py b/src/gbasm/instructions/nop.py new file mode 100644 index 0000000..3ef2922 --- /dev/null +++ b/src/gbasm/instructions/nop.py @@ -0,0 +1,24 @@ +from .Instruction import Instruction +from ..arguments import Argument +from typing import Callable, List + + +class Nop(Instruction): + def __init__(self): + argtypes = [[]] + super().__init__("NOP", argtypes) + + def num_bytes(self, arguments) -> int: + return 1 + + def to_bytes( + self, + arguments: List[Argument], + instruction_addr: int, + label_resolver: Callable[[str], int], + ) -> bytes: + + if len(arguments) != 0: + raise ValueError("Incorrect number of arguments") + + return bytes([0x00]) diff --git a/src/gbasm/instructions/stop.py b/src/gbasm/instructions/stop.py new file mode 100644 index 0000000..d1c6fe9 --- /dev/null +++ b/src/gbasm/instructions/stop.py @@ -0,0 +1,24 @@ +from .Instruction import Instruction +from ..arguments import Argument +from typing import Callable, List + + +class Stop(Instruction): + def __init__(self): + argtypes = [[]] + super().__init__("STOP", argtypes) + + def num_bytes(self, arguments) -> int: + return 1 + + def to_bytes( + self, + arguments: List[Argument], + instruction_addr: int, + label_resolver: Callable[[str], int], + ) -> bytes: + + if len(arguments) != 0: + raise ValueError("Incorrect number of arguments") + + return bytes([0x10]) diff --git a/test/cases/format.yaml b/test/cases/format.yaml new file mode 100644 index 0000000..d1049a9 --- /dev/null +++ b/test/cases/format.yaml @@ -0,0 +1,20 @@ +--- +name: two_nop +program: | + NOP + NOP +expected: + - 0x00 + - 0x00 +--- +name: unused_labels +program: | + start: + NOP + middle: + NOP + end: +expected: + - 0x00 + - 0x00 +--- diff --git a/test/cases/format/format.yaml b/test/cases/format/format.yaml new file mode 100644 index 0000000..0d5777f --- /dev/null +++ b/test/cases/format/format.yaml @@ -0,0 +1,19 @@ +--- +name: two_nop +program: | + NOP + NOP +expected: + - 0x00 + - 0x00 +--- +name: unused_labels +program: | + start: + NOP + middle: + NOP + end: +expected: + - 0x00 + - 0x00 diff --git a/test/cases/instructions/dec.yaml b/test/cases/instructions/dec.yaml new file mode 100644 index 0000000..fe99a23 --- /dev/null +++ b/test/cases/instructions/dec.yaml @@ -0,0 +1,74 @@ +--- +name: dec_a +program: | + DEC A +expected: + - 0x3D + +--- +name: dec_b +program: | + DEC B +expected: + - 0x05 +--- +name: dec_c +program: | + DEC C +expected: + - 0x0D +--- +name: dec_d +program: | + DEC D +expected: + - 0x15 +--- +name: dec_e +program: | + DEC E +expected: + - 0x1D +--- +name: dec_h +program: | + DEC H +expected: + - 0x25 +--- +name: dec_l +program: | + DEC L +expected: + - 0x2D +--- +name: dec_(hl) +program: | + DEC (HL) +expected: + - 0x35 + +--- +name: dec_bc +program: | + DEC BC +expected: + - 0x0B +--- +name: dec_de +program: | + DEC DE +expected: + - 0x1B +--- +name: dec_hl +program: | + DEC HL +expected: + - 0x2B +--- +name: dec_sp +program: | + DEC SP +expected: + - 0x3B diff --git a/test/cases/instructions/inc.yaml b/test/cases/instructions/inc.yaml new file mode 100644 index 0000000..40b0d79 --- /dev/null +++ b/test/cases/instructions/inc.yaml @@ -0,0 +1,74 @@ +--- +name: inc_a +program: | + INC A +expected: + - 0x3c + +--- +name: inc_b +program: | + INC B +expected: + - 0x04 +--- +name: inc_c +program: | + INC C +expected: + - 0x0c +--- +name: inc_d +program: | + INC D +expected: + - 0x14 +--- +name: inc_e +program: | + INC E +expected: + - 0x1c +--- +name: inc_h +program: | + INC H +expected: + - 0x24 +--- +name: inc_l +program: | + INC L +expected: + - 0x2c +--- +name: inc_(hl) +program: | + INC (HL) +expected: + - 0x34 + +--- +name: inc_bc +program: | + INC BC +expected: + - 0x03 +--- +name: inc_de +program: | + INC DE +expected: + - 0x13 +--- +name: inc_hl +program: | + INC HL +expected: + - 0x23 +--- +name: inc_sp +program: | + INC SP +expected: + - 0x33 diff --git a/test/cases/instructions/jr.yaml b/test/cases/instructions/jr.yaml new file mode 100644 index 0000000..c5aabe9 --- /dev/null +++ b/test/cases/instructions/jr.yaml @@ -0,0 +1,370 @@ +--- +name: jr_bkwd +program: | + label: + JR label +expected: + - 0x18 + - 0xFE +--- +name: jr_fwd +program: | + JR label + label: +expected: + - 0x18 + - 0x00 + +--- +name: jr_z_bkwd +program: | + label: + JR Z label +expected: + - 0x28 + - 0xFE +--- +name: jr_z_fwd +program: | + JR Z label + label: +expected: + - 0x28 + - 0x00 + +--- +name: jr_nz_bkwd +program: | + label: + JR NZ label +expected: + - 0x20 + - 0xFE +--- +name: jr_nz_fwd +program: | + JR NZ label + label: +expected: + - 0x20 + - 0x00 + +--- +name: jr_c_bkwd +program: | + label: + JR C label +expected: + - 0x38 + - 0xFE +--- +name: jr_c_fwd +program: | + JR C label + label: +expected: + - 0x38 + - 0x00 + +--- +name: jr_nc_bkwd +program: | + label: + JR NC label +expected: + - 0x30 + - 0xFE +--- +name: jr_nc_fwd +program: | + JR NC label + label: +expected: + - 0x30 + - 0x00 + +--- +# Jump backward by the maximum amount +name: jr_far_bkwd +program: | + far_label: + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + JR far_label +expected: [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x80 ] + +--- +# Jump forward by the maximum amount +name: jr_far_fwd +program: | + JR far_label + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + NOP + far_label: +expected: [ 0x18, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ] diff --git a/test/cases/instructions/ld_reg16.yaml b/test/cases/instructions/ld_reg16.yaml new file mode 100644 index 0000000..790fdf2 --- /dev/null +++ b/test/cases/instructions/ld_reg16.yaml @@ -0,0 +1,81 @@ +--- +name: ld_bc +program: | + LD BC 0 +expected: + - 0x01 + - 0x00 + - 0x00 +--- +name: ld_de +program: | + LD DE 0 +expected: + - 0x11 + - 0x00 + - 0x00 +--- +name: ld_hl +program: | + LD HL 0 +expected: + - 0x21 + - 0x00 + - 0x00 +--- +name: ld_sp +program: | + LD SP 0 +expected: + - 0x31 + - 0x00 + - 0x00 + +--- +name: ld_16_max +program: | + LD BC 65535 +expected: + - 0x01 + - 0xFF + - 0xFF +--- +name: ld_16_negative +program: | + LD BC -1 +expected: + - 0x01 + - 0xFF + - 0xFF +--- +name: ld_16_min +program: | + LD BC -32768 +expected: + - 0x01 + - 0x00 + - 0x80 +--- +name: ld_16_00FF +program: | + LD BC 0x00FF +expected: + - 0x01 + - 0xFF + - 0x00 +--- +name: ld_16_FF00 +program: | + LD BC 0xFF00 +expected: + - 0x01 + - 0x00 + - 0xFF +--- +name: ld_16_100 +program: | + LD BC 10 +expected: + - 0x01 + - 0x0A + - 0x00 diff --git a/test/cases/instructions/special.yaml b/test/cases/instructions/special.yaml new file mode 100644 index 0000000..0651f7a --- /dev/null +++ b/test/cases/instructions/special.yaml @@ -0,0 +1,12 @@ +--- +name: nop +program: | + NOP +expected: + - 0x00 +--- +name: stop +program: | + STOP +expected: + - 0x10 diff --git a/test/test_assemble.py b/test/test_assemble.py new file mode 100644 index 0000000..bb29491 --- /dev/null +++ b/test/test_assemble.py @@ -0,0 +1,66 @@ +from gbasm.gbasm import assemble + +from pathlib import Path +import os +import yaml +import pytest +import logging + +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + logging.basicConfig(format="") + logging.getLogger().setLevel(logging.INFO) + + +class AssembleCase(object): + def __init__(self, name: str, program: str, expected: bytes): + self.name = name + self.program = program + self.expected = expected + + +def find_case_files(subdir: str): + test_root = Path(os.path.dirname(os.path.abspath(__file__))) + case_root = test_root / "cases" / subdir + return case_root.glob("**/*.yaml") + + +def get_test_cases(subdir: str): + cases = [] + files = find_case_files(subdir) + for f in files: + index = 0 + with open(str(f), "r") as yaml_file: + test_descs = yaml.safe_load_all(yaml_file) + for desc in test_descs: + try: + case = AssembleCase( + desc["name"], desc["program"], bytes(desc["expected"]) + ) + except TypeError: + logger.exception("Failed to parse yaml: %s", desc) + cases.append(case) + return cases + + +instruction_cases = get_test_cases("instructions") + + +@pytest.mark.parametrize( + "case", instruction_cases, ids=[case.name for case in instruction_cases] +) +def test_assemble_instruction(case): + lines = case.program.split("\n") + assembled = assemble(lines) + assert assembled == case.expected + + +format_cases = get_test_cases("format") + + +@pytest.mark.parametrize("case", format_cases, ids=[case.name for case in format_cases]) +def test_format_instruction(case): + lines = case.program.split("\n") + assembled = assemble(lines) + assert assembled == case.expected