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)

11
asm/test.ant Normal file
View File

@@ -0,0 +1,11 @@
SET r5 0xaa
main:
ID r0
SET r5 0x55
ADD r5 0x5
RSHIFT r5 4
LSHIFT r5 4
RANDOM r1 0x1f
RANDOM r2 10
SNIFF CH_RED
MOVE RANDOM

135
asm/twotrail.ant Normal file
View File

@@ -0,0 +1,135 @@
; ── SWARM FORAGER ────────────────────────────
; Random walk to find food, dead reckoning to
; navigate home. Pheromone marks food for others.
; Hit play, then try improving it!
; ── Registers ──
.alias dx r1 ; x displacement from nest
.alias dy r2 ; y displacement from nest
.alias ret r3 ; return address for CALL
; ── Pheromone ──
.const PH_FOOD CH_RED ; marks food locations
.const PH_NEST CH_BLUE
.const PHER_INIT 255
.const PHER_DECAY 2
; ── Roles (visible in simulation viewer) ──
.tag 0 foraging
.tag 1 homing
; ──────────────────────────────────────────────
main:
set r7 PHER_INIT
loop:
CARRYING
JNE r0 0 go_home
TAG foraging
; ── SEARCH: find food ─────────────────────────
search:
SENSE FOOD
JNE r0 0 grab
; if we are too far from the food, dont mark
JLT r7 PHER_DECAY smell_trail
SUB r7 PHER_DECAY
; if there is already a trail here, dont mark
SNIFF PH_NEST HERE r0
JNE r0 0 smell_trail
MARK PH_NEST r7
smell_trail:
SMELL PH_FOOD
JNE r0 0 step
SENSE EMPTY
step:
CALL ret move_track
JMP loop
grab:
CALL ret move_track
PICKUP
MARK PH_FOOD 255
SET r6 PHER_INIT
JMP loop
; ── GO HOME: dead reckon toward nest ──────────
go_home:
TAG homing
; if we are too far from the food, dont mark
JLT r6 PHER_DECAY sense_nest
SUB r6 PHER_DECAY
; if there is already a trail here, dont mark
SNIFF PH_FOOD HERE r0
JNE r0 0 sense_nest
MARK PH_FOOD r6
sense_nest:
SENSE NEST
JNE r0 0 deliver
smell_nest:
SMELL PH_NEST
JNE r0 0 move_track
RANDOM r0 4
JEQ r0 0 wander ; 25% random to unstick walls
CALL ret home_dir
CALL ret move_track
JMP main
wander:
SENSE EMPTY
CALL ret move_track
JMP main
deliver:
CALL ret move_track
DROP
SET r7 PHER_INIT
JMP main
; ══════════════════════════════════════════════
; SUBROUTINES — CALL ret <label>, return JMP ret
; ══════════════════════════════════════════════
; home_dir: set r0 = direction toward nest
home_dir:
SET r4 dx
JGT dx 0 hd_dxp
SET r4 0
SUB r4 dx
hd_dxp:
SET r5 dy
JGT dy 0 hd_dyp
SET r5 0
SUB r5 dy
hd_dyp:
JGT r5 r4 hd_y
JGT dx 0 hd_w
SET r0 2
JMP ret
hd_w:
SET r0 4
JMP ret
hd_y:
JGT dy 0 hd_n
SET r0 3
JMP ret
hd_n:
SET r0 1
JMP ret
; move_track: MOVE r0, then update dx/dy
move_track:
MOVE r0
JEQ r0 1 mt_n
JEQ r0 2 mt_e
JEQ r0 3 mt_s
SUB dx 1
JMP ret
mt_n:
SUB dy 1
JMP ret
mt_e:
ADD dx 1
JMP ret
mt_s:
ADD dy 1
JMP ret