#!/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 = []