#!/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]')