#!/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("> 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)