154 lines
4.7 KiB
Python
Executable File
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()
|