239 lines
8.9 KiB
Python
239 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
# a simple assembler/disassembler for the n808 VM
|
||
|
|
# supports both text and binary program formats
|
||
|
|
# usage: n8asm [at|ab|dt|db|t2b|b2t|t74] file outfile
|
||
|
|
# modes:
|
||
|
|
# at: assemble to plaintext (N8) machine code
|
||
|
|
# ab: assemble to binary (N8B) machine code
|
||
|
|
# dt: disassemble plaintext (N8) machine code
|
||
|
|
# db: disassemble binary (N8B) machine code
|
||
|
|
# t2b: convert from plaintext N8 to binary N8B
|
||
|
|
# b2t: convert from binary N8B to plaintext N8
|
||
|
|
# t74: convert from plaintext N8 to the TI-74 representation
|
||
|
|
# Created by Luxferre in 2025, released into public domain
|
||
|
|
|
||
|
|
import sys, re, struct
|
||
|
|
|
||
|
|
# n808 mnemonics for assembly and disasssembly
|
||
|
|
mnemos = ['nop', 'jmp', 'iat', 'ino', 'cpy', 'set', 'mat', 'rnd']
|
||
|
|
|
||
|
|
# n808 jump/function shortcuts (arranged from longest to shortest)
|
||
|
|
jmpfunc_shorts = {
|
||
|
|
'ret': 'jmp 13 0 125', # return from a procedure
|
||
|
|
'nnn': 'nop 0 0 0', # alias for nop
|
||
|
|
'inc': 'mat 0 127', # increment
|
||
|
|
'dec': 'mat 0 126', # decrement
|
||
|
|
'inv': 'mat 3 127', # inverse/reciprocal
|
||
|
|
'iuc': 'jmp 13 0', # indirect unconditional jump
|
||
|
|
'jpr': 'jmp 14 0', # jump to a procedure
|
||
|
|
'neg': 'mat 1 0', # negation
|
||
|
|
'juc': 'jmp 6 0', # direct unconditional jump
|
||
|
|
'ige': 'jmp 10', # indirect jump if greater than or equals to zero
|
||
|
|
'ile': 'jmp 11', # indirect jump if less than or equals to zero
|
||
|
|
'ine': 'jmp 12', # indirect jump if not equals to zero
|
||
|
|
'cos': 'mat 10', # cosine
|
||
|
|
'atn': 'mat 11', # arctangent
|
||
|
|
'jeq': 'jmp 0', # direct jump if equals to zero
|
||
|
|
'jgt': 'jmp 1', # direct jump if greater than zero
|
||
|
|
'jlt': 'jmp 2', # direct jump if less than zero
|
||
|
|
'jge': 'jmp 3', # direct jump if greater than or equals to zero
|
||
|
|
'jle': 'jmp 4', # direct jump if less than or equals to zero
|
||
|
|
'jne': 'jmp 5', # direct jump if not equals to zero
|
||
|
|
'ieq': 'jmp 7', # indirect jump if equals to zero
|
||
|
|
'igt': 'jmp 8', # indirect jump if greater than zero
|
||
|
|
'ilt': 'jmp 9', # indirect jump if less than zero
|
||
|
|
'out': 'ino 0', # normal numeric output
|
||
|
|
'inp': 'ino 1', # normal numeric input
|
||
|
|
'ouc': 'ino 2', # character output
|
||
|
|
'ipc': 'ino 3', # character input
|
||
|
|
'dca': 'cpy 0', # direct constant assignment
|
||
|
|
'dva': 'cpy 1', # direct value assignment
|
||
|
|
'ica': 'cpy 2', # indirect constant assignment
|
||
|
|
'iva': 'cpy 3', # indirect value assignment
|
||
|
|
'ivc': 'cpy 4', # indirect value copy
|
||
|
|
'add': 'mat 0', # addition
|
||
|
|
'sub': 'mat 1', # subtraction
|
||
|
|
'mul': 'mat 2', # multiplication
|
||
|
|
'div': 'mat 3', # division
|
||
|
|
'mdf': 'mat 4', # modulo/floor
|
||
|
|
'abs': 'mat 5', # absolute value
|
||
|
|
'sqr': 'mat 6', # square root
|
||
|
|
'exp': 'mat 7', # natural exponent
|
||
|
|
'log': 'mat 8', # natural logarithm
|
||
|
|
'sin': 'mat 9' # sine
|
||
|
|
}
|
||
|
|
|
||
|
|
# converts the N8A source code to the plaintext machine code representation
|
||
|
|
def assemble(source):
|
||
|
|
labels = {}
|
||
|
|
out = ''
|
||
|
|
lno = 0
|
||
|
|
vreg = re.compile(r'\s+')
|
||
|
|
for shrt, meaning in jmpfunc_shorts.items(): # replace the shortcuts
|
||
|
|
source = source.replace(shrt, meaning)
|
||
|
|
for line in source.split('\n'): # parse the main code
|
||
|
|
fields = []
|
||
|
|
line = line.split(';')[0].strip()
|
||
|
|
if len(line) > 0: # actual line to be counted on
|
||
|
|
fields = vreg.split(line)[:5] # split it into fields
|
||
|
|
if fields[0].startswith(':'): # this is a label
|
||
|
|
labels[fields[0]] = lno # remember the label
|
||
|
|
fields = fields[1:] # remove the label from fields
|
||
|
|
elif fields[0].startswith('#'): # this is an alias
|
||
|
|
labels['@' + fields[1].strip()] = int(fields[0][1:])
|
||
|
|
continue # do not update the instruction number
|
||
|
|
try:
|
||
|
|
cmd = mnemos.index(fields[0].lower())
|
||
|
|
except ValueError:
|
||
|
|
try:
|
||
|
|
cmd = int(cmd)
|
||
|
|
except:
|
||
|
|
print('Assembly error - unknown mnemonic at line', lno)
|
||
|
|
sys.exit(1)
|
||
|
|
try:
|
||
|
|
out += ' '.join([str(cmd), fields[1], fields[2], fields[3]]) + '\n'
|
||
|
|
except IndexError:
|
||
|
|
print('Assembly error - not enough operands at line', lno)
|
||
|
|
sys.exit(1)
|
||
|
|
lno += 1 # update the instruction number
|
||
|
|
for lbl, lineno in labels.items():
|
||
|
|
out = out.replace(lbl, str(lineno))
|
||
|
|
final = '' # prepare the final text
|
||
|
|
for line in out.split('\n'):
|
||
|
|
if len(line) > 0:
|
||
|
|
fields = vreg.split(line)[:4] # split it into fields
|
||
|
|
instr = int(fields[0]) * 2097152 + int(fields[1]) * 16384
|
||
|
|
instr += int(fields[2]) * 128 + int(fields[3])
|
||
|
|
final += str(instr) + '\n'
|
||
|
|
return final
|
||
|
|
|
||
|
|
# converts the plaintext machine code representation to N8A source code
|
||
|
|
def disassemble(machcode):
|
||
|
|
vreg = re.compile(r'\s+')
|
||
|
|
iindex = 0
|
||
|
|
instrs = []
|
||
|
|
for instrnum in vreg.split(machcode):
|
||
|
|
if len(instrnum) > 0:
|
||
|
|
instrnum = int(instrnum)
|
||
|
|
opcode = (instrnum >> 21) & 7
|
||
|
|
p1 = (instrnum >> 14) & 127
|
||
|
|
p2 = (instrnum >> 7) & 127
|
||
|
|
p3 = instrnum & 127
|
||
|
|
instrs.append([iindex, opcode, p1, p2, p3])
|
||
|
|
iindex += 1
|
||
|
|
# now, instrs contains 5-number groups
|
||
|
|
assembly = [None for i in range(0, 128)]
|
||
|
|
labels = {}
|
||
|
|
for instr in instrs: # iterate over each instruction
|
||
|
|
lno, cmd, x, y, z = instr
|
||
|
|
try:
|
||
|
|
mnemo = mnemos[cmd]
|
||
|
|
except IndexError:
|
||
|
|
print('Disassembly error - unknown opcode at line', lno)
|
||
|
|
sys.exit(1)
|
||
|
|
if cmd == 1: # save the label for jump instructions
|
||
|
|
labels[z] = ':lbl_' + str(z)
|
||
|
|
assembly[lno] = [mnemo, str(x), str(y), labels[z]]
|
||
|
|
else:
|
||
|
|
assembly[lno] = [mnemo, str(x), str(y), str(z)]
|
||
|
|
out = ''
|
||
|
|
lno = 0
|
||
|
|
for asline in assembly:
|
||
|
|
if lno in labels:
|
||
|
|
out += labels[lno] + ' '
|
||
|
|
if asline is not None:
|
||
|
|
out += ' '.join(asline) + '\n'
|
||
|
|
lno += 1
|
||
|
|
for shrt, meaning in jmpfunc_shorts.items(): # replace the shortcuts
|
||
|
|
out = out.replace(meaning, shrt)
|
||
|
|
return out
|
||
|
|
|
||
|
|
# converts the plaintext machine code representation to binary representation
|
||
|
|
def tobinary(txtrep):
|
||
|
|
instrs = [] # instruction list to store here
|
||
|
|
vreg = re.compile(r'\s+')
|
||
|
|
out = b''
|
||
|
|
for instr in vreg.split(txtrep):
|
||
|
|
if len(instr) > 0:
|
||
|
|
instr = int(instr)
|
||
|
|
out += struct.pack('BBB', (instr >> 16) & 255, (instr >> 8) & 255, instr & 255)
|
||
|
|
return out
|
||
|
|
|
||
|
|
# converts the binary machine code representation to plaintext representation
|
||
|
|
def totext(binrep):
|
||
|
|
out = ''
|
||
|
|
while len(binrep) > 0:
|
||
|
|
chunk = binrep[0:3]
|
||
|
|
binrep = binrep[3:]
|
||
|
|
b1, b2, b3 = struct.unpack('BBB', chunk)
|
||
|
|
instr = (b1 << 16) | (b2 << 8) | b3
|
||
|
|
out += str(instr) + '\n'
|
||
|
|
return out
|
||
|
|
|
||
|
|
# converts the N8 plaintext machine code to the TI-74 DATA statements (N74)
|
||
|
|
def ti74data(txtrep:str, baseaddr:int=1000):
|
||
|
|
instrs = [] # instruction list to store here
|
||
|
|
vreg = re.compile(r'\s+')
|
||
|
|
out = ''
|
||
|
|
iindex = 0
|
||
|
|
for instr in vreg.split(txtrep):
|
||
|
|
if len(instr) > 0:
|
||
|
|
instrs.append(str(int(instr)))
|
||
|
|
instrs.append(str(-1)) # add the terminating instruction
|
||
|
|
while len(instrs) > 0:
|
||
|
|
dchunk = instrs[:7]
|
||
|
|
instrs = instrs[7:]
|
||
|
|
out += f'{baseaddr + iindex} DATA {','.join(dchunk)}\n'
|
||
|
|
iindex += 1
|
||
|
|
return out
|
||
|
|
|
||
|
|
def main():
|
||
|
|
if len(sys.argv) < 4:
|
||
|
|
print("Usage: n8asm [at|ab|dt|db|t2b|b2t|t74] file outfile")
|
||
|
|
return
|
||
|
|
mode = sys.argv[1]
|
||
|
|
srcfile = sys.argv[2]
|
||
|
|
targetfile = sys.argv[3]
|
||
|
|
srctext = ''
|
||
|
|
inpmode = 'r'
|
||
|
|
if mode == 'b2t' or mode == 'db':
|
||
|
|
inpmode = 'rb'
|
||
|
|
try:
|
||
|
|
fd = open(srcfile, inpmode)
|
||
|
|
srctext = fd.read()
|
||
|
|
close(fd)
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
if len(srctext) > 0:
|
||
|
|
outmode = 'w'
|
||
|
|
output = ''
|
||
|
|
if mode == 'b2t': # binary-to-text
|
||
|
|
output = totext(srctext)
|
||
|
|
elif mode == 't2b': # text-to-binary
|
||
|
|
output = tobinary(srctext)
|
||
|
|
outmode = 'wb'
|
||
|
|
elif mode == 'dt': # disassemble text
|
||
|
|
output = disassemble(srctext)
|
||
|
|
elif mode == 'db': # disassemble binary
|
||
|
|
srctext = totext(srctext)
|
||
|
|
output = disassemble(srctext)
|
||
|
|
elif mode == 't74': # convert to TI-74 DATA statements
|
||
|
|
output = ti74data(srctext)
|
||
|
|
else: # assemble
|
||
|
|
output = assemble(srctext) # default mode
|
||
|
|
if mode == 'ab':
|
||
|
|
output = tobinary(output)
|
||
|
|
outmode = 'wb'
|
||
|
|
# write the output file
|
||
|
|
try:
|
||
|
|
fd = open(targetfile, outmode)
|
||
|
|
fd.write(output)
|
||
|
|
close(fd)
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
print('Nothing to process!')
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|