This commit is contained in:
Richard
2026-03-26 18:43:01 +01:00
commit 9616a8236b
25 changed files with 2773 additions and 0 deletions

239
asm/asm.py Executable file
View File

@@ -0,0 +1,239 @@
#!/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)