gigatron/rom/Core/vasm.py
2025-01-28 19:17:01 +03:00

197 lines
7.6 KiB
Python

# SYNOPSIS: from vasm import *
#
# This is the (back end for) a minimalistic vCPU assembler in Python.
# Use Python itself as the front end.
#
# As always vCPU mmenomnics are upper case
# Special words:
# ORG(address) Start new segment
# L('Name') Create a label
# BYTE(byte,...) Insert data
# END(address) Finish assembly, address is execution address
import io
import json
import sys
# The _gt1 variable holds all the segments of the resulting GT1 file.
# It is organized as a list of 4-tuples (one such 4-tuple per segment).
# The format of the 4-tuple is `(start_addr, size, labels, content)`,
# where:
# - start_addr : is the start address of the segment. This is typically
# a number, but it can be None (for floating segments that
# will be placed/resolved later via the RESOLVE_SEGMENTS()
# function);
# - size : the size of the segment;
# - labels : the dictionary of labels defined within the segment;
# The format of this dictionary is { label : offset, ... },
# where label is the string of the label, and offset is
# an integer offset where the label was defined (relative
# to the start of the segment).
# Per-segment label/symbol tables are used because of the
# floating segments that might not yet have fixed locations;
# - content: the list with the contents of the segment. Each entry
# is a byte, a string, or a pair. Strings and pairs are
# resolved by _eval() at the end of the assembly process
# (when the user invokes END()). Strings are resolved
# via the _symbols table, and pairs are resolved as follows.
# When an entry is a pair (say x), the first entry x[0]
# of the pair is a callback that will be invoked by _eval()
# with the argument _eval(x[1]). Note that x[1] itself can
# be a byte, a string, or a pair. This will allow for
# recursive evaluations of these pairs.
_gt1 = [(0x200, 0x100, {}, []) ]
# [ start_addr, size, {label : offset, ...}, [byte, ...],
# start_addr, size, {label : offset, ...}, [byte, ...], ...]
# `byte' can be int, symbol string, or expression tuple
_symbols = {} # name -> value
# Preload system-defined symbols
with open('interface.json') as file:
for (name, value) in json.load(file).items():
_symbols[name] = value if isinstance(value, int) else int(value, base=0)
_emit_callback = None
def ORG(addr, size=0x100, callback=None):
# The callback is invoked before every _emit() within
# the segment defined by this ORG().
# The callback should accept two parameters: (gt1, ins)
# where gt1 is a copy of the current _gt1 and ins is
# the tuple containing the instructions to be inserted
# in the contents of the last segment.
# The callback can be used, for example, to automatically
# create new segments when current segment is full.
global _emit_callback
_gt1.append((addr, size, {}, []))
_emit_callback = callback
def LDWI(op): return _emit((0x11, (LO,op), (HI,op)))
def LD(op): return _emit((0x1a, op))
def LDW(op): return _emit((0x21, op))
def STW(op): return _emit((0x2b, op))
def BEQ(op): return _emit((0x35, 0x3f, (_br,op)))
def BGT(op): return _emit((0x35, 0x4d, (_br,op)))
def BLT(op): return _emit((0x35, 0x50, (_br,op)))
def BGE(op): return _emit((0x35, 0x53, (_br,op)))
def BLE(op): return _emit((0x35, 0x56, (_br,op)))
def LDI(op): return _emit((0x59, op))
def ST(op): return _emit((0x5e, op))
def POP(): return _emit((0x63,))
def BNE(op): return _emit((0x35, 0x72, (_br,op)))
def PUSH(): return _emit((0x75,))
def LUP(op): return _emit((0x7f, op))
def ANDI(op): return _emit((0x82, op))
def ORI(op): return _emit((0x88, op))
def XORI(op): return _emit((0x8c, op))
def BRA(op): return _emit((0x90, (_br,op)))
def INC(op): return _emit((0x93, op))
def ADDW(op): return _emit((0x99, op))
def PEEK(): return _emit((0xad,))
def SYS(op): return _emit((0xb4, 270-op//2 if op>28 else 0))
def SUBW(op): return _emit((0xb8, op))
def DEF(op): return _emit((0xcd, (_br,op)))
def CALL(op): return _emit((0xcf, op))
def ALLOC(op): return _emit((0xdf, op))
def ADDI(op): return _emit((0xe3, op))
def SUBI(op): return _emit((0xe6, op))
def LSLW(): return _emit((0xe9,))
def STLW(op): return _emit((0xec, op))
def LDLW(op): return _emit((0xee, op))
def POKE(op): return _emit((0xf0, op))
def DOKE(op): return _emit((0xf3, op))
def DEEK(): return _emit((0xf6,))
def ANDW(op): return _emit((0xf8, op))
def ORW(op): return _emit((0xfa, op))
def XORW(op): return _emit((0xfc, op))
def RET(): return _emit((0xff,))
def CALLI(op): return _emit((0x85, (LO,op), (HI,op)))
def CMPHS(op): return _emit((0x1f, op))
def CMPHU(op): return _emit((0x97, op))
def BYTE(*op): return _emit(op)
def L(name):
if name in _symbols:
ERR('Redefined %s' % repr(name))
_emit([]) # To call the emit callback before we define labels
# This is done here to prevent labels from being
# created at the end of full segments (in case
# the callback creates a new segment).
segment = _gt1[-1]
segment[2][name] = len(segment[3])
if segment[0] is not None:
# If the symbol can be resolved now, resolve it already
_symbols[name] = segment[0] + len(segment[3])
else:
_symbols[name] = None
def ALIGN(nbytes=2):
segment = _gt1[-1]
addr = segment[0] + len(segment[3])
rem = addr % nbytes
if rem != 0:
_emit(tuple([0] * (nbytes - rem)))
def RESOLVE_SEGMENTS(callback):
# The RESOLVE_SEGMENTS() function is mainly used to resolve
# the `floating` segments, i.e. segments without a defined start address.
# The callback is then responsible for modifying _gt1 to place and
# resolve these segments.
return callback(_gt1, _symbols)
def END(start=0x200, filename='out.gt1', resolve_callback=None):
if resolve_callback is not None:
RESOLVE_SEGMENTS(resolve_callback)
if filename is not None:
_f = open(filename, 'wb')
else:
_f = io.BytesIO()
with _f as f:
for segment in _gt1:
address, size, labels, contents = segment
if len(contents) > 0:
if len(contents) > size:
ERR('Segment too large at 0x%04X' % address)
if address + len(contents) > (address | 255) + 1:
ERR('Page overrun in segment 0x%04X' % address)
resolved = [_byte(_eval(x)) for x in contents]
f.write(bytes([address >> 8, address & 255, len(resolved) & 255]))
f.write(bytes(resolved))
start = _eval(start)
f.write(bytes([0, start >> 8, start & 255]))
if filename is None:
return f.getvalue()
def _emit(ins):
if _emit_callback is not None:
_emit_callback(_gt1, ins)
segment = _gt1[-1]
segment[3].extend(ins)
return 0
def LO(x): return _eval(x) & 255 # Low byte of word
def HI(x): return _eval(x) >> 8 # High byte of word
def ADDR(x): return _eval(x) # The address of the label
def _br(x): return (_eval(x) - 2) & 255 # Adjust for pre-increment of vPC
def _eval(x):
fn = lambda x: x # No operation
if isinstance(x, tuple): # Tuple expressions
fn, x = x[0], _eval(x[1])
if isinstance(x, str): # Resolve symbol strings
x = _symbols[x] if x in _symbols else ERR('Undefined %s' % repr(x))
return fn(x)
def _byte(x):
if x < -128 or x > 255:
ERR('Out of byte range: %s' % repr(x))
return x & 255
def ERR(*args):
line = 'Error: ' + ' '.join(args)
print('\033[1m' + line + '\033[0m' if sys.stdout.isatty() else line)
sys.exit(1)