Files
mu808/mu808.py
T

227 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
# mu808 VM reference implementation in Python 3 / MicroPython
# See the documentation in README.md
# Created by Luxferre in 2025, released into public domain
import sys, math, random, re
advterm = False
try:
import tty, termios # for the character input port
advterm = True
except:
pass
MEMLIMIT:int = 16384 # 16K floats for data, 64K integers for program
PMEM = [[0,0,0,0] for i in range(0, MEMLIMIT)] # program memory
DMEM = [0.0 for i in range(0, MEMLIMIT)] # data memory
traceflag:bool = False
runlimit:int = MEMLIMIT # default runlimit is the program space size
runcount:int = 0
# mu808 mnemonics for assembly and disasssembly
mnemos = ['nop', 'jmp', 'iat', 'out', 'inp', 'set', 'cpy', 'fma',
'sub', 'div', 'mdf', 'abs', 'sqr', 'nel', 'tri', 'rnd']
# port output
def portout(port:int, data:float):
if port == 0: # standard value output port
print(data)
elif port == 1: # character output port
sys.stdout.write(chr(int(data)&255))
sys.stdout.flush()
# port input
def portin(port:int):
val = 0
if port == 0: # standard input port
try:
val = float(input())
except ValueError:
val = 0
if port == 2: # character input port
if advterm: # normal OS with termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
else: # crippled OS without termios
ch = sys.stdin.read(1)
val = ord(ch)
return float(val)
# instruction line execution function
# the main mu808 logic is defined here
def ilexec(lno:int, cmd:int=0, x:int=0, y:int=0, z:int=0):
global runcount, PMEM, DMEM
halt = False
while not halt:
if traceflag:
print(f'PC: {lno} INSTR: {cmd} {x} {y} {z}')
DMEM[0] = 0.0 # force the value at 0 to always be 0
DMEM[127] = 1.0 # force the value at 127 to always be 1
data_override = False
# perform a boundary check and prefetch the memory values
bcheck = True
try:
v1 = DMEM[x]
v2 = DMEM[y]
v3 = DMEM[z]
except IndexError:
bcheck = False
if bcheck:
# command switch starts here (commands <=0 and >15 are nops)
if cmd == 1: # JMP: conditional/unconditional, direct/indirect jump
if (v2 == 0 and x == 0) or (v2 > 0 and x == 1) or (v2 < 0 and x == 2) \
or (v2 >= 0 and x == 3) or (v2 <= 0 and x == 4) or (v2 != 0 and x == 5) \
or x == 6:
halt = False
lno = z - 1
elif (v2 == 0 and x == 7) or (v2 > 0 and x == 8) or (v2 < 0 and x == 9) \
or (v2 >= 0 and x == 10) or (v2 <= 0 and x == 11) or (v2 != 0 and x == 12) \
or x == 13:
halt = False
lno = int(v3) - 1
elif cmd == 2: # IAT
data_override = True
elif cmd == 3: # OUT
for i in range(x, y + 1):
portout(z, DMEM[i])
elif cmd == 4: # INP
for i in range(x, y + 1):
DMEM[i] = portin(z)
elif cmd == 5: # SET
DMEM[z] = (x % 10000) + (y % 10000) / 10000.
elif cmd == 6: # CPY
if x == 0:
DMEM[z] = y
elif x == 1:
DMEM[z] = v2
elif int(v3) < MEMLIMIT and int(v2) < MEMLIMIT:
DMEM[int(v3)] = DMEM[int(v2)]
elif cmd == 7: # FMA (v1 + v2 * v3)
try:
DMEM[z] = math.fma(v3, v2, v1)
except:
DMEM[z] = v1 + v2 * v3
elif cmd == 8: # SUB
DMEM[z] = v1 - v2
elif cmd == 9: # DIV
if v2 == 0:
DMEM[z] = 0
else:
DMEM[z] = v1 / v2
elif cmd == 10: # MDF
if v2 == 0:
DMEM[z] = math.floor(v1)
else:
DMEM[z] = float(int(v1) % int(v2))
elif cmd == 11: # ABS
DMEM[z] = math.fabs(v2)
elif cmd == 12: # SQR
DMEM[z] = math.sqrt(math.fabs(v2))
elif cmd == 13: # NEL
if x == 0:
DMEM[z] = math.exp(v2)
else:
if v2 == 0:
DMEM[z] = 0
else:
DMEM[z] = math.log(math.fabs(v2))
elif cmd == 14: # TRI
if x == 0:
DMEM[z] = math.sin(v2)
elif x == 1:
DMEM[z] = math.cos(v2)
else:
DMEM[z] = math.atan(v2)
elif cmd == 15: # RND
DMEM[z] = float(random.randint(int(v1), int(v2)))
# command switch ends here
# increment the program counter
lno += 1
if lno >= MEMLIMIT or lno < 1 or (runlimit > 0 and runcount > runlimit):
if traceflag:
print('Memory limit or runlimit hit, halting...')
halt = True
else: # fetch the next instruction
runcount += 1
if data_override and bcheck:
cmd = PMEM[lno][0]
x = int(v1) % MEMLIMIT
y = int(v2) % MEMLIMIT
z = int(v3) % MEMLIMIT
else:
cmd, x, y, z = tuple(PMEM[lno])
# instruction line entry function
def ilenter(lno:int, cmd:int=0, x:int=0, y:int=0, z:int=0):
global traceflag, runlimit, runcount
if lno > 0: # record the instruction line in memory
PMEM[lno] = [cmd, x, y, z]
elif lno == 0: # immediately execute the instruction line
runcount = 0
ilexec(lno, cmd, x, y, z)
elif lno == -1: # display a range of instructions from cmd to x
for i in range(cmd, x + 1):
print(f'@{i}:\t{mnemos[PMEM[i][0]]}|{PMEM[i][0]}', PMEM[i][1],
PMEM[i][2], PMEM[i][3])
elif lno == -2: # clear a range of data or instructions from x to y
for i in range(x, y + 1):
try:
if cmd == 0:
PMEM[i] = [0,0,0,0]
else:
DMEM[i] = 0.0
except IndexError:
pass
elif lno == -3: # turn on/off tracing
if cmd == 0:
traceflag = False
print('Tracing off')
else:
traceflag = True
print('Tracing on')
elif lno == -4: # set the runlimit to the value of cmd
runlimit = cmd
print('Runlimit set to', runlimit)
elif lno == -5: # exit to the environment
print('Bye!')
sys.exit(0)
# main REPL environment
if __name__ == '__main__':
print(f'mu808 v1 by Luxferre\nPROGMEM: {MEMLIMIT} steps\nDATAMEM: {MEMLIMIT} floats')
vreg = re.compile(r'[^-\d]+')
if len(sys.argv) > 1: # preload the file contents
fc = ''
try:
fd = open(sys.argv[1], 'r')
fc = fd.read()
close(fd)
except:
pass
instr = [] # current instruction cache
for instrpart in vreg.split(fc):
if len(instrpart) > 0:
instr.append(int(instrpart))
if len(instr) == 5: # full instruction registered
ilenter(int(instr[0]), abs(int(instr[1])),
abs(int(instr[2])), abs(int(instr[3])), abs(int(instr[4])))
instr = []
instr = [] # current instruction cache
while True: # main interactive loop
rinput = input('> ')
if len(rinput) > 0:
for v in vreg.split(rinput): # loop over the current input
if len(v) > 0:
instr.append(v)
if len(instr) == 5: # full instruction registered
ilenter(int(instr[0]), abs(int(instr[1])),
abs(int(instr[2])), abs(int(instr[3])), abs(int(instr[4])))
instr = []