240 lines
7.6 KiB
Python
Executable File
240 lines
7.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import logging
|
|
from typing import List
|
|
from dataclasses import dataclass
|
|
from struct import pack
|
|
|
|
class AssemblerError(Exception):
|
|
pass
|
|
|
|
@dataclass
|
|
class Instruction:
|
|
name: str
|
|
number: int
|
|
load_jr: bool = False
|
|
|
|
class Assembler:
|
|
def __init__(self):
|
|
self.prog = bytearray()
|
|
self.lineno = 0
|
|
self.labels = {}
|
|
self.label_references = []
|
|
self.logger = logging.getLogger("asm")
|
|
instructions = [
|
|
Instruction("NOOP", 0), # Come on, we just need one
|
|
Instruction("ID", 1),
|
|
Instruction("TAG", 2),
|
|
|
|
Instruction("MOVE", 3),
|
|
Instruction("SENSE", 4),
|
|
Instruction("PROBE", 5),
|
|
|
|
Instruction("PICKUP", 6),
|
|
Instruction("CARRYING", 7),
|
|
Instruction("DROP", 8),
|
|
|
|
Instruction("SMELL", 9),
|
|
Instruction("SNIFF", 0xa),
|
|
Instruction("MARK", 0xb),
|
|
|
|
Instruction("SET", 0x10),
|
|
Instruction("AND", 0x11),
|
|
Instruction("OR", 0x12),
|
|
Instruction("XOR", 0x13),
|
|
Instruction("LSHIFT", 0x14),
|
|
Instruction("RSHIFT", 0x15),
|
|
Instruction("ADD", 0x16),
|
|
Instruction("SUB", 0x17),
|
|
Instruction("MUL", 0x18),
|
|
Instruction("DIV", 0x19),
|
|
Instruction("MOD", 0x1a),
|
|
Instruction("RANDOM", 0x1b),
|
|
|
|
Instruction("SJR", 0x20), # New 'hidden' instruction to support jmp instructions
|
|
Instruction("MJR", 0x21), # New 'hidden' instruction to support jmp instruction from reg
|
|
Instruction("JMP", 0x22, True),
|
|
Instruction("CALL", 0x23, True),
|
|
Instruction("JEQ", 0x24, True),
|
|
Instruction("JNE", 0x25, True),
|
|
Instruction("JGT", 0x26, True),
|
|
Instruction("JLT", 0x27, True)
|
|
|
|
|
|
]
|
|
self.instructions = {i.name:i for i in instructions}
|
|
self.constants = {
|
|
"r0": 0x80,
|
|
"r1": 0x81,
|
|
"r2": 0x82,
|
|
"r3": 0x83,
|
|
"r4": 0x84,
|
|
"r5": 0x85,
|
|
"r6": 0x86,
|
|
"r7": 0x87,
|
|
"jr": 0x88,
|
|
"empty": 0,
|
|
"wall": 1,
|
|
"food": 2,
|
|
"nest": 3,
|
|
"ant": 4,
|
|
"here": 0,
|
|
"n": 1,
|
|
"e": 2,
|
|
"s": 3,
|
|
"w": 4,
|
|
"north": 1,
|
|
"east": 2,
|
|
"south": 3,
|
|
"west": 4,
|
|
"random": 5,
|
|
"ch_red": 0,
|
|
"ch_blue": 1,
|
|
"ch_green": 2,
|
|
"ch_yellow": 3,
|
|
|
|
}
|
|
|
|
def write(self, bt):
|
|
if type(bt) == int:
|
|
self.prog += bytes([bt])
|
|
else:
|
|
self.prog += bt
|
|
#print(self.prog.hex())
|
|
|
|
def handle_label(self, iname):
|
|
lblname = iname[:-1].lower()
|
|
if lblname in self.labels:
|
|
raise AssemblerError(f"Label {lblname} defined twice")
|
|
self.labels[lblname] = len(self.prog) // 4
|
|
|
|
def handle_directive(self, iname, args):
|
|
if iname in ['.alias', '.const', '.tag']:
|
|
source = args[0] if iname == '.tag' else args[1]
|
|
dest = args[1].lower() if iname == '.tag' else args[0].lower()
|
|
|
|
self.logger.debug(f"{iname} {dest} = {self.resolve_arg(source)}")
|
|
if dest in self.constants:
|
|
raise AssemblerError(f"Overwriting const {dest}")
|
|
self.constants[dest] = self.resolve_arg(source)
|
|
else:
|
|
raise AssemblerError(f"Invalid directive {iname}")
|
|
|
|
def resolve_arg(self, arg, raise_if_fail=True):
|
|
arg = arg.lower()
|
|
if arg in self.constants:
|
|
result = self.constants[arg]
|
|
elif arg.isnumeric():
|
|
result = int(arg)
|
|
elif arg.startswith("0x"):
|
|
result = int(arg, 16)
|
|
elif raise_if_fail:
|
|
raise AssemblerError(f"Cant parse argument {arg}")
|
|
else:
|
|
result = 0
|
|
if result > 255:
|
|
raise AssemblerError(f"Arg {arg} value bigger than 255")
|
|
return result
|
|
|
|
def handle_load_jr(self, instruction, target):
|
|
target_reg = self.resolve_arg(target, raise_if_fail=False)
|
|
if instruction.name == "JMP" and target_reg in range(0x80, 0x88):
|
|
mjr = self.instructions["MJR"]
|
|
data = [mjr.number, target_reg, 0, 0]
|
|
else:
|
|
sjr = self.instructions["SJR"]
|
|
data = [sjr.number, 0, 0, 0]
|
|
offset = len(self.prog) + 1
|
|
self.label_references.append((target, offset))
|
|
self.write(bytes(data))
|
|
|
|
def handle_instruction(self, iname, args):
|
|
instruction = self.instructions[iname.upper()]
|
|
if len(args) > 3:
|
|
raise AssemblerError("Too many arguments")
|
|
data = [instruction.number]
|
|
if instruction.load_jr:
|
|
target = args[-1]
|
|
self.handle_load_jr(instruction, target)
|
|
args = args[:-1]
|
|
for i in range(3):
|
|
if i >= len(args):
|
|
data.append(0)
|
|
else:
|
|
data.append(self.resolve_arg(args[i]))
|
|
|
|
self.write(bytes(data))
|
|
|
|
def assemble_line(self, line):
|
|
pre_offset = len(self.prog)
|
|
line = line.strip()
|
|
# remove comments
|
|
parts = line.split(";", 1)
|
|
if len(parts) == 2:
|
|
line = parts[0]
|
|
|
|
# split into parts
|
|
parts = line.split()
|
|
if len(parts) == 0:
|
|
return
|
|
iname = parts[0]
|
|
|
|
if iname.endswith(":"):
|
|
self.handle_label(iname)
|
|
elif iname.startswith("."):
|
|
self.handle_directive(iname, parts[1:])
|
|
elif iname.upper() in self.instructions:
|
|
self.handle_instruction(iname, parts[1:])
|
|
else:
|
|
raise AssemblerError(f"Instruction {iname} not found")
|
|
new_data = self.prog[pre_offset:]
|
|
self.logger.debug(f"{line.strip()} : {new_data.hex()}")
|
|
|
|
def resolve_references(self):
|
|
for reflabel, refoffset in self.label_references:
|
|
if not reflabel in self.labels:
|
|
raise AssemblerError(f"Could not resolve label {reflabel}")
|
|
lblloc = self.labels[reflabel]
|
|
self.prog[refoffset] = lblloc & 0xff
|
|
self.prog[refoffset + 1] = lblloc >> 8 & 0xff
|
|
self.prog[refoffset + 2] = lblloc >> 16 & 0xff
|
|
|
|
def assemble_text(self, data):
|
|
for line in data.split("\n"):
|
|
self.lineno += 1
|
|
self.assemble_line(line)
|
|
self.resolve_references()
|
|
return self.prog
|
|
|
|
def assemble_file(self, inf, outf):
|
|
with open(inf) as asm:
|
|
assy = asm.read()
|
|
|
|
assy = "JMP main\n" + assy
|
|
prog = self.assemble_text(assy)
|
|
|
|
output = sys.stdout.buffer if outf is None else open(outf, 'wb')
|
|
output.write(pack("<I", len(prog) >> 2)) # This is the header of the file: num_instructions
|
|
output.write(prog)
|
|
if output != sys.stdout:
|
|
output.close()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
assembler = Assembler()
|
|
parser = argparse.ArgumentParser(description='Assemble a program.')
|
|
parser.add_argument('file', help='File to assemble')
|
|
parser.add_argument('--output', metavar='FILE',
|
|
help='Output file (stdout if ommitted)')
|
|
parser.add_argument('--debug', '-d', action='store_true')
|
|
args = parser.parse_args()
|
|
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
|
try:
|
|
assembler.assemble_file(args.file, args.output)
|
|
except AssemblerError as e:
|
|
sys.stderr.write(f"Error at line: {assembler.lineno}: {e}\n")
|
|
sys.exit(1)
|