Created
August 30, 2013 17:41
Revisions
-
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,503 @@ #!/usr/local/bin/python3.2 """ Python interpreter for the esoteric language ><> (pronounced /ˈfɪʃ/). Usage: ./fish.py --help More information: http://esolangs.org/wiki/Fish Requires python 2.7/3.2 or higher. """ import sys import time import random from collections import defaultdict # constants NCHARS = "0123456789abcdef" ARITHMETIC = "+-*%" # not division, as it requires special handling COMPARISON = { "=": "==", "(": "<", ")": ">" } DIRECTIONS = { ">": (1,0), "<": (-1,0), "v": (0,1), "^": (0,-1) } MIRRORS = { "/": lambda x,y: (-y, -x), "\\": lambda x,y: (y, x), "|": lambda x,y: (-x, y), "_": lambda x,y: (x, -y), "#": lambda x,y: (-x, -y) } class _Getch: """ Provide cross-platform getch functionality. Shamelessly stolen from http://code.activestate.com/recipes/134892/ """ def __init__(self): try: self._impl = _GetchWindows() except ImportError: self._impl = _GetchUnix() def __call__(self): return self._impl() class _GetchUnix: def __init__(self): import tty, sys def __call__(self): import sys, tty, termios fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class _GetchWindows: def __init__(self): import msvcrt def __call__(self): import msvcrt return msvcrt.getch() getch = _Getch() def read_character(): """Read one character from stdin. Returns -1 when no input is available.""" if sys.stdin.isatty(): # we're in console, read a character from the user char = getch() # check for ctrl-c (break) if ord(char) == 3: sys.stdout.write("^C") sys.stdout.flush() raise KeyboardInterrupt else: return char else: # input is redirected using pipes char = sys.stdin.read(1) # return -1 if there is no more input available return char if char != "" else -1 class Interpreter: """ ><> "compiler" and interpreter. """ def __init__(self, code): """ Initialize a new interpreter. Arguments: code -- the code to execute as a string """ # check for hashbang in first line lines = code.split("\n") if lines[0][:2] == "#!": code = "\n".join(lines[1:]) # construct a 2D defaultdict to contain the code self._codebox = defaultdict(lambda: defaultdict(int)) line_n = char_n = 0 for char in code: if char != "\n": self._codebox[line_n][char_n] = 0 if char == " " else ord(char) char_n += 1 else: char_n = 0 line_n += 1 self._position = [-1,0] self._direction = DIRECTIONS[">"] # the register is initially empty self._register_stack = [None] # string mode is initially disabled self._string_mode = None # have we encountered a skip instruction? self._skip = False self._stack = [] self._stack_stack = [self._stack] # is the last outputted character a newline? self._newline = None def move(self): """ Move one step in the execution process, and handle the instruction (if any) at the new position. """ # move one step in the current direction self._position[0] += self._direction[0] self._position[1] += self._direction[1] # wrap around if we reach the borders of the codebox if self._position[1] > max(self._codebox.keys()): # if the current position is beyond the number of lines, wrap to # the top self._position[1] = 0 elif self._position[1] < 0: # if we're above the top, move to the bottom self._position[1] = max(self._codebox.keys()) if self._direction[0] == 1 and self._position[0] > max(self._codebox[self._position[1]].keys()): # wrap to the beginning if we are beyond the last character on a # line and moving rightwards self._position[0] = 0; elif self._position[0] < 0: # also wrap if we reach the left hand side self._position[0] = max(self._codebox[self._position[1]].keys()) # execute the instruction found if not self._skip: instruction = int(self._codebox[self._position[1]][self._position[0]]) # the current position might not be a valid character try: # use space if current cell is 0 instruction = chr(instruction) if instruction > 0 else " " except: instruction = None try: self._handle_instruction(instruction) except StopExecution: raise except KeyboardInterrupt: # avoid catching as error raise KeyboardInterrupt except Exception as e: raise StopExecution("something smells fishy...") return instruction self._skip = False def _handle_instruction(self, instruction): """ Execute an instruction. """ if instruction == None: # error on invalid characters raise Exception # handle string mode if self._string_mode != None and self._string_mode != instruction: self._push(ord(instruction)) return elif self._string_mode == instruction: self._string_mode = None return # instruction is one of ^v><, change direction if instruction in DIRECTIONS: self._direction = DIRECTIONS[instruction] # direction is a mirror, get new direction elif instruction in MIRRORS: self._direction = MIRRORS[instruction](*self._direction) # pick a random direction elif instruction == "x": self._direction = random.choice(list(DIRECTIONS.items()))[1] # portal; move IP to coordinates elif instruction == ".": y, x = self._pop(), self._pop() # IP cannot reach negative codebox if x < 0 or y < 0: raise Exception self._position = [x,y] # instruction is 0-9a-f, push corresponding hex value elif instruction in NCHARS: self._push(int(instruction, len(NCHARS))) # instruction is an arithmetic operator elif instruction in ARITHMETIC: a, b = self._pop(), self._pop() exec("self._push(b{}a)".format(instruction)) # division elif instruction == ",": a, b = self._pop(), self._pop() # try converting them to floats for python 2 compability try: a, b = float(a), float(b) except OverflowError: pass self._push(b/a) # comparison operators elif instruction in COMPARISON: a, b = self._pop(), self._pop() exec("self._push(1 if b{}a else 0)".format(COMPARISON[instruction])) # turn on string mode elif instruction in "'\"": # turn on string parsing self._string_mode = instruction # skip one command elif instruction == "!": self._skip = True # skip one command if popped value is 0 elif instruction == "?": if not self._pop(): self._skip = True # push length of stack elif instruction == "l": self._push(len(self._stack)) # duplicate top of stack elif instruction == ":": self._push(self._stack[-1]) # remove top of stack elif instruction == "~": self._pop() # swap top two values elif instruction == "$": a, b = self._pop(), self._pop() self._push(a) self._push(b) # swap top three values elif instruction == "@": a, b, c = self._pop(), self._pop(), self._pop() self._push(a) self._push(c) self._push(b) # put/get register elif instruction == "&": if self._register_stack[-1] == None: self._register_stack[-1] = self._pop() else: self._push(self._register_stack[-1]) self._register_stack[-1] = None # reverse stack elif instruction == "r": self._stack.reverse() # right-shift stack elif instruction == "}": self._push(self._pop(), index=0) # left-shift stack elif instruction == "{": self._push(self._pop(index=0)) # get value in codebox elif instruction == "g": x, y = self._pop(), self._pop() self._push(self._codebox[x][y]) # set (put) value in codebox elif instruction == "p": x, y, z = self._pop(), self._pop(), self._pop() self._codebox[x][y] = z # pop and output as character elif instruction == "o": self._output(chr(int(self._pop()))) # pop and output as number elif instruction == "n": n = self._pop() # try outputting without the decimal point if possible self._output(int(n) if int(n) == n else n) # get one character from input and push it elif instruction == "i": i = self._input() self._push(ord(i) if isinstance(i, str) else i) # pop x and create a new stack with x members moved from the old stack elif instruction == "[": count = int(self._pop()) if count == 0: self._stack_stack[-1], new_stack = self._stack, [] else: self._stack_stack[-1], new_stack = self._stack[:-count], self._stack[-count:] self._stack_stack.append(new_stack) self._stack = new_stack # create a new register for this stack self._register_stack.append(None) # remove current stack, moving its members to the previous stack. # if this is the last stack, a new, empty stack is pushed elif instruction == "]": old_stack = self._stack_stack.pop() if not len(self._stack_stack): self._stack_stack.append([]) else: self._stack_stack[-1] += old_stack self._stack = self._stack_stack[-1] # register is dropped self._register_stack.pop() if not len(self._register_stack): self._register_stack.append(None) # the end elif instruction == ";": raise StopExecution() # space is NOP elif instruction == " ": pass # invalid instruction else: raise Exception("Invalid instruction", instruction) def _push(self, value, index=None): """ Push a value to the current stack. Keyword arguments: index -- the index to push/insert to. (default: end of stack) """ self._stack.insert(len(self._stack) if index == None else index, value) def _pop(self, index=None): """ Pop and return a value from the current stack. Keyword arguments: index -- the index to pop from (default: end of stack) """ # don't care about exceptions - they are handled at a higher level value = self._stack.pop(len(self._stack)-1 if index == None else index) # convert to int where possible to avoid float overflow if value == int(value): value = int(value) return value def _input(self): """ Return an inputted character. """ return read_character() def _output(self, output): """ Output a string without a newline appended. """ output = str(output) self._newline = output.endswith("\n") sys.stdout.write(output) sys.stdout.flush() class StopExecution(Exception): """ Exception raised when a script has finished execution. """ def __init__(self, message=None): self.message = message if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description=""" Execute a ><> script. Executing a script is as easy as: %(prog)s <script file> You can also execute code directly using the -c/--code flag: %(prog)s -c '1n23nn;' > 132 The -v and -s flags can be used to prepopulate the stack: %(prog)s echo.fish -s "hello, world" -v 32 49 50 51 -s "456" > hello, world 123456""", usage="""%(prog)s [-h] (<script file> | -c <code>) [<options>]""", formatter_class=argparse.RawDescriptionHelpFormatter) group = parser.add_argument_group("code") # group script file and --code together to only allow one code_group = group.add_mutually_exclusive_group(required=True) code_group.add_argument("script", type=argparse.FileType("r"), nargs="?", help=".fish file to execute") code_group.add_argument("-c", "--code", metavar="<code>", help="string of instructions to execute") options = parser.add_argument_group("options") options.add_argument("-s", "--string", action="append", metavar="<string>", dest="stack") options.add_argument("-v", "--value", type=float, nargs="+", action="append", metavar="<number>", dest="stack", help="push numbers or strings onto the stack before execution starts") options.add_argument("-t", "--tick", type=float, default=0.0, metavar="<seconds>", help="define a tick time, or a delay between the execution of each instruction") options.add_argument("-a", "--always-tick", action="store_true", default=False, dest="always_tick", help="make every instruction cause a tick (delay), even whitespace and skipped instructions") # parse arguments from sys.argv arguments = parser.parse_args() # initialize an interpreter if arguments.script: code = arguments.script.read() arguments.script.close() else: code = arguments.code interpreter = Interpreter(code) # add supplied values to the interpreters stack if arguments.stack: for x in arguments.stack: if isinstance(x, str): interpreter._stack += [float(ord(c)) for c in x] else: interpreter._stack += x # run the script try: while True: try: instr = interpreter.move() except StopExecution as stop: # only print a newline if the script didn't newline = ("\n" if (not interpreter._newline) and interpreter._newline != None else "") parser.exit(message=(newline+stop.message+"\n") if stop.message else newline) if instr and not instr == " " or arguments.always_tick: time.sleep(arguments.tick) except KeyboardInterrupt: # exit cleanly parser.exit(message="\n")