Files
2022-09-03 07:58:16 +03:00

228 lines
9.2 KiB
Python

#!/usr/bin/env python3
# The reference assembler for NRJ OISC (tested for NRJ16)
# By Luxferre, 2022, public domain
import sys
import array
from os.path import realpath
from numpy import trim_zeros
# some constants to redefine more easily in case of major breaking changes
NRJDEF_BITS = 16 # default word/addr size if the .bit directive is omitted
# service characters
NRJCHAR_COMMENT = ';' # all comments are after ;
NRJCHAR_VARDEREF = '@' # variable dereferencing is with @
NRJCHAR_CHARDEREF = "'" # character dereferencing is with '
NRJCHAR_SUBST = '%' # var-in-macro substitution is with %
# macro variables
NRJVAR1 = NRJCHAR_SUBST + 'A' # %A
NRJVAR2 = NRJCHAR_SUBST + 'B' # %B
NRJVAR3 = NRJCHAR_SUBST + 'C' # %C
# and preprocessor directives
NRJDIR_INC = '.inc'
NRJDIR_BITS = '.bit'
NRJDIR_ORG = '.org'
NRJDIR_DEF = '.def'
NRJDIR_END = '.end'
NRJDIR_VAR = '.var'
NRJDIR_SET = '.set'
NRJDIR_FREE = 'FREE'
NRJDIR_NXT = 'NXT'
NRJDIR_HLT = 'HLT'
included_files = [] # stash to check the already included files
def readsrc(fname): # read source file contents, stripping comments, empty lines and trailing/leading whitespace
f = open(fname, 'r')
rawlines = f.readlines()
f.close()
lines = []
global included_files
included_files.append(realpath(fname))
for line in rawlines:
line = line.split(NRJCHAR_COMMENT)[0].strip()
if len(line) > 0:
tokens = line.split() # split on any whitespace, which is what we need
if tokens[0] == NRJDIR_INC: # process include directive immediately
incfname = realpath(' '.join(tokens[1:])) # because the name may include spaces
if incfname not in included_files: # cyclic inclusion protection
incfile = readsrc(incfname) # call itself recursively, trying to include a file
included_files.append(incfname) # update the list of included files
lines.extend(incfile) # update the source with the included contents in place
else:
print('Attempt to include an already included file %s, ignoring!' % incfname)
else: # otherwise just append the tokenized source line
lines.append(tokens)
return lines
def start_assembly(srcfname, dstfname): # main assembly method
wordsize = NRJDEF_BITS # define machine word/address size
# we're starting with tokenized Stage 1 source: all includes processed, comments and whitespace stripped
stage1src = readsrc(srcfname)
# Stage 2: scan the source for the first word size set directive
for line in stage1src:
if line[0] == NRJDIR_BITS:
wordsize = int(line[1])
break
print('Building for NRJ%u' % wordsize)
# Stage 3: expand all macros
stage3src = []
macrobuffers = {}
macrostart = False
macroname = None
for line in stage1src:
if macrostart: # we already are buffering a macro
if line[0] == NRJDIR_END: # macro ended and saved in the buffers
macrostart = False
macroname = None
else: # continue buffering
macrobuffers[macroname].append(line)
else: # usual code
if line[0] == NRJDIR_DEF: # starting a macro
macroname = line[1]
macrobuffers[macroname] = [] # prepare the place to buffer the macro into
macrostart = True
elif line[0] != NRJDIR_BITS: # ignoring word size directive as we already processed it
if line[0] in macrobuffers: # detected an already compiled macro, substituting the code and parameters
p1 = NRJDIR_HLT # placeholders for missing parameters
p2 = NRJDIR_HLT
p3 = NRJDIR_NXT # assume we're referring to the next address in p3
if len(line) > 1: # fill the first parameter if present
p1 = line[1]
if len(line) > 2: # fill the second parameter if present
p2 = line[2]
if len(line) > 3: # fill the third parameter if present
p3 = line[3]
for macroline in macrobuffers[line[0]]: # now, perform the macrosubstitution with parameter replacement
stage3src.append(' '.join(macroline).replace(NRJVAR1, p1).replace(NRJVAR2, p2).replace(NRJVAR3, p3).split())
else: # append a normal line
stage3src.append(line)
# Stage 4: now, process .var directive, FREE directive, @ and ' dereferencing operators
vartable = {} # don't store numeric locations here yet, only string representations (hex or FREE)
stage4src = []
for line in stage3src:
if line[0] == NRJDIR_VAR: # .var directive: no @ or ' operators allowed here
if line[2] == NRJDIR_FREE:
vartable[line[1]] = 0
else:
vartable[line[1]] = int(line[2], 16)
# now, fill in the FREE bits
maxvar = 0
for vname in vartable:
if vartable[vname] > maxvar:
maxvar = vartable[vname]
for vname in vartable:
if vartable[vname] == 0:
maxvar += 1
vartable[vname] = maxvar
for line in stage3src:
if line[0] != NRJDIR_VAR: # finally, perform variable substitution
# but first, attempt to perform character substitution
for i, el in enumerate(line):
if el.startswith(NRJCHAR_CHARDEREF):
line[i] = hex(ord(el[1]))[2:].upper()
sline = ' '.join(line)
for vname in vartable:
sline = sline.replace(NRJCHAR_VARDEREF+vname, hex(vartable[vname])[2:].upper())
stage4src.append(sline.split())
# now, our Stage 4 code is fully flat and we can start allocating memory for it
# directives left to process at this point: .org, .set, NXT, HLT
# (we cannot process .set before because it can also take value of NXT or HLT)
memsize = 1 << wordsize
haltaddr = memsize - 1 # halting address to be filled in the lookup table
print('Allocating %u %u-bit words of memory...' % (memsize, wordsize))
memmod = 'H'
if wordsize >= 32:
memmod = 'L'
elif wordsize >= 64:
memmod = 'Q'
elif wordsize <= 8:
memmod = 'B'
targetmem = array.array(memmod, [0]*memsize)
# here is the trickiest part of the whole assembly process - building a lookup table
# as NRJ can't directly jump to the next instruction by itself, we need to tell it to
# the NXT macro will be replaced with a cell in the lookup table that points to the next instruction
# and the lookup table will also take some memory in the machine
# now, try to detect the optimal offset for our lookup table
if maxvar > 0: # we have some variables defined, so place the lookup table after them
ltoffset = maxvar + 1
else: # in the worst case scenario, the code will take half of all memory and lookup table will take the other half
ltoffset = memsize >> 1
targetmem[ltoffset] = haltaddr # the first lookup table entry is always the halting address
codepos = 0
ltpos = 1
# let's iterate over the code
# pass 1
for line in stage4src:
if line[0] == NRJDIR_ORG: # handle .org
codepos = int(line[1], 16)
elif line[0] == NRJDIR_SET: # handle .set
addr = int(line[1], 16)
val = line[2]
if val == NRJDIR_HLT:
targetmem[addr] = ltoffset
elif val != NRJDIR_NXT:
targetmem[addr] = int(val, 16)
else: # 3-value vector where HLT or NXT can be encountered
# save current instruction in the lookup table
targetmem[ltoffset + ltpos] = codepos
ltpos += 1
for v in line:
if v == NRJDIR_HLT:
targetmem[codepos] = ltoffset
elif v == NRJDIR_NXT:
targetmem[codepos] = ltoffset + ltpos
else:
targetmem[codepos] = int(v, 16)
codepos += 1
# pass 2 - fill in NXT
codepos = 0
ltpos = 1
for line in stage4src:
if line[0] == NRJDIR_ORG: # handle .org
codepos = int(line[1], 16)
elif line[0] == NRJDIR_SET: # handle .set
addr = int(line[1], 16)
val = line[2]
if val == NRJDIR_NXT:
val = targetmem[ltoffset + ltpos]
targetmem[addr] = val
else: # 3-value vector where HLT or NXT can be encountered
ltpos += 1
for v in line:
codepos += 1
# looking for a more optimal solution than to import numpy just for this:
targetmem = trim_zeros(targetmem, 'b') # only strip trailing zero values
# now, we have assembled our target memory snapshot, let's write the output file
outf = open(dstfname, "wb")
targetmem.tofile(outf)
outf.close()
print('Assembled %s' % dstfname)
if __name__ == '__main__': # nrjasm entry point
version = '0.0.1'
print('nrjasm v%s by Luxferre, 2022' % version)
if len(sys.argv) > 2:
print('Assembling %s into %s...' % (sys.argv[1], sys.argv[2]))
start_assembly(sys.argv[1], sys.argv[2])
else:
print('Usage: nrjasm.py [source] [binary]')