#!/usr/bin/python3 """Dumber interpreter. Last night I tried to do a BASIC, but I got snarled up in debugging my Packrat parsing engine, and so I never got to actually writing the interpreter. So this time I think I'm going to do a dumber language, in a shorter time, instead of starting with a parser, and see if I can generate a fractal with the interpreter. """ from __future__ import print_function, division import re def interpret(instructions): ram = {} stack = [] pc = 0 done = False while not done: done, pc = instructions[pc](ram, stack) language = [] def directive(pattern): "A construct of the language occupying one line, including meta-level stuff." def wrap(f): language.append((re.compile(r'\s*' + pattern + '$'), f)) return f return wrap def cmd(pattern): """An ordinary construct of the language with straight-through control flow. This is a special case of ``directive`` above, for cases that don’t interact with labels or unresolved or halt the program, just continue to the next instruction. """ def wrap2(f): @directive(pattern) def cmd_handler(mo, labels, unresolved, pc): def execute(ram, stack): f(ram, stack, *mo.groups()) return False, pc+1 return [execute] return wrap2 @directive(r'(\w+):\s*') def handle_label(mo, labels, unresolved, pc): label = mo.group(1) if label in unresolved: for item in unresolved[label]: item(pc) del unresolved[label] labels[label] = pc return [] @directive(r'-> (\w+)') def handle_jump(mo, labels, unresolved, pc): label = mo.group(1) dest = labels.get(label) if dest is None: def resolve(new_dest): nonlocal dest dest = new_dest if label not in unresolved: unresolved[label] = [] unresolved[label].append(resolve) def jump(ram, stack): return False, dest return [jump] @directive(r'(\w+)\s*\?\s*(\w+)') def handle_may_jump(mo, labels, unresolved, pc): v = mo.group(1) label = mo.group(2) # Hmm, is this a better way to do it? Maybe also factor dest into # a thunk. dest = None def resolve(new_dest): nonlocal dest dest = new_dest if label in labels: resolve(labels[label]) else: if label not in unresolved: unresolved[label] = [] unresolved[label].append(resolve) def may_jump(ram, stack): if ram[v]: return False, dest return False, pc+1 return [may_jump] @directive(r'bye\s*') def handle_bye(mo, labels, unresolved, pc): def bye(ram, stack): return True, None return [bye] @directive(r'--.*') def handle_comment(mo, labels, unresolved, pc): return [] @cmd(r'say\s*"((?:""|[^"])*)"') def say_s(ram, stack, s): print(s.replace('""', '"'), end='') @cmd(r'say\s+(\w+)') def say_v(ram, stack, v): print(ram[v], end='') @cmd(r'putc\s+(\w+)') def putc_v(ram, stack, v): print(chr(ram[v] & 0xff), end='') @cmd(r'(\w+)\s*=\s*(-?\d+)') def load_const(ram, stack, v, n): ram[v] = int(n) # Ordering is significant: \d are also \w @cmd(r'(\w+)\s*=\s*(\w+)') def mov(ram, stack, v, v2): ram[v] = ram[v2] @cmd(r'(\w+)\s*\+=\s*(\w+)') def inc(ram, stack, v, v2): ram[v] += ram[v2] @cmd(r'(\w+)\s*\*=\s*(\w+)') def mul(ram, stack, v, v2): ram[v] *= ram[v2] @cmd(r'(\w+)\s*/=\s*(\w+)') def div(ram, stack, v, v2): ram[v] //= ram[v2] @cmd(r'(\w+)\s*-=\s*(\w+)') def sub(ram, stack, v, v2): ram[v] -= ram[v2] @cmd(r'relu\s*(\w+)') def relu(ram, stack, v): ram[v] = max(0, ram[v]) @cmd(r'my\s*(\w+)\[(\w+)\]') def allocate(ram, stack, name, size): ram[name] = [0] * ram[size] @cmd(r'(\w+)\[(\w+)\]\s*=\s*(\w+)') def istore(ram, stack, name, index, value): ram[name][ram[index]] = ram[value] @cmd(r'(\w+)\s*=\s*(\w+)\[(\w+)\]') def ifetch(ram, stack, dest, src, index): ram[dest] = ram[src][ram[index]] @directive(r'') def nop(mo, labels, unresolved, pc): return [] def compile(lines): insns = [] labels = {} unresolved = {} for line in lines: for pattern, action in language: mo = pattern.match(line) if mo is None: continue results = action(mo, labels, unresolved, len(insns)) insns.extend(results) break else: raise ValueError("unparsable line %r" % line) if unresolved: raise ValueError("unresolved labels: %s" % ', '.join(unresolved.keys())) return insns # A broken implementation of Minsky's circle algorithm. test_program_1 = """ start: nl = 10 -> skip say "this never happens" skip: -- say "raw 8-bit audio follows: " say "om " x = 100 y = 0 epsilon = 3 delta = -3 scale = 100 continue: dx = y dx *= epsilon dx /= scale say "dx = " say dx dy = x dy *= delta dy /= scale x += dx y += dy -- putc x say " " say x putc nl diff = x limit = 1000 diff -= limit relu diff diff ? abort -> continue abort: say "too big" putc nl bye """ sierpinski_program = """ n = 128 my stars[n] i = 0 init = 1 stars[i] = init nl = 10 t = 0 Δt = 1 loop: i = 0 Δi = 1 iloop: i ? update -> endif2 update: new = stars[i] oldi = i oldi -= Δi old = stars[oldi] new += old -- okay, now we know the number of stars above and to the left. -- We want to update stars[i] to be 0 if that was 2, 1 if it was 1, -- and 0 if it was 0. The zero case is trivial; nothing need be done. new ? notzero -> done_updating notzero: -- Now we must distinguish between 1 and 2. Let's make them different. Δ = 1 new -= Δ new ? two_stars -- There must have been 1. new_val = 1 stars[i] = new_val -> done_updating two_stars: new_val = 0 stars[i] = new_val done_updating: endif2: s = stars[i] s ? paint say " " -> endif paint: say "*" endif: i += Δi left = n left -= i relu left left ? iloop putc nl t += Δt left = 128 left -= t relu left left ? loop bye say "impossible" """ if __name__ == '__main__': prog = compile(sierpinski_program.split('\n')) interpret(prog)