Files
gb-emu/src/python/src/gbasm/gbasm.py

154 lines
4.7 KiB
Python
Executable File

#!/usr/bin/python3
import argparse
import logging
import sys
from .instructions import Instruction
from .instructions.inc import Inc
from .arguments import ArgumentType, Argument
from typing import Callable, Dict, List, Optional
logger = logging.getLogger(__name__)
COMMENT_CHAR = '#'
LABEL_SUFFIX = ':'
GB_INSTRUCTIONS = [
Inc()
]
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[ArgumentType]) -> 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],
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, 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: {}".format(instruction_map))
byte_offset = 0
instruction_count = 0
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: {}".format(step))
for line_num, line in enumerate(lines):
# Remove comments
line = line.split(COMMENT_CHAR)[0]
# Tokenize
tokens = line.split()
logging.info("Line:", line)
logging.info("Tokens:", tokens)
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 '{}' at {}"
.format(label, byte_offset))
if label in labels.keys():
raise KeyError("Label '{}' defined at {} and {}"
.format(label, labels[label], line_num))
labels[label] = byte_offset
continue
raise KeyError("Unknown instruction \"{}\" on line {}"
.format(instruction_name, line_num))
if step == 'SIZE':
byte_offset += parse_line_size(instruction, args)
instruction_count += 1
if step == 'CONTENT':
try:
program += parse_line_bytes(instruction, args, label_resolver)
except ValueError:
raise ValueError("Failed to parse line {},\n{}"
.format(line_num, line))
if step == 'SIZE':
logger.info("Program size: {} bytes, {} instructions"
.format(byte_offset, instruction_count))
logger.debug("Found labels: {}".format(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()