Skip to content

Instantly share code, notes, and snippets.

@lyxal
Forked from anonymous/fish.py
Last active August 13, 2020 05:00
Show Gist options
  • Save lyxal/bb351e36e1ab3867194b6f5442f650eb to your computer and use it in GitHub Desktop.
Save lyxal/bb351e36e1ab3867194b6f5442f650eb to your computer and use it in GitHub Desktop.
><> python interpreter.
#!/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 = [1]
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")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment