7 Commits

23 changed files with 1358 additions and 1 deletions

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ build/*
doxygen/*
buildbot-upload/*
*/config.mak
*\~
*\~
__pycache__

View File

View File

@@ -0,0 +1,7 @@
class ArgumentParser(object):
def can_parse(token: str) -> bool:
raise NotImplementedError()
def get_name() -> str:
raise NotImplementedError()

View File

@@ -0,0 +1,138 @@
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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
from .ArgumentParser import ArgumentParser
from .ArgumentParsers import Label, Address, Immediate8, Immediate16, Register8, Register16
from .Arguments import Argument

157
src/python/src/gbasm/gbasm.py Executable file
View File

@@ -0,0 +1,157 @@
#!/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(infile)
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"), 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)
outfile.write(program)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,17 @@
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()

View File

@@ -0,0 +1 @@
from . Instruction import Instruction

View File

@@ -0,0 +1,51 @@
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))

View File

@@ -0,0 +1,51 @@
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))

View File

@@ -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)

View File

@@ -0,0 +1,45 @@
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")

View File

@@ -0,0 +1,23 @@
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])

View File

@@ -0,0 +1,23 @@
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])

View File

@@ -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
---

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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, ]

View File

@@ -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

View File

@@ -0,0 +1,12 @@
---
name: nop
program: |
NOP
expected:
- 0x00
---
name: stop
program: |
STOP
expected:
- 0x10

View File

@@ -0,0 +1,58 @@
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