diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da8c130 --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# PyCharm +.idea/ + +/befunge/_version.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7fe8e4 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Befunge +[Befunge](https://en.wikipedia.org/wiki/Befunge) interpreter and debugger for Befunge 93, +the first of the [Funges](https://web.archive.org/web/20041225010717/http://quadium.net/funge/spec98.html). + +## Installation +`pip install befunge@git+https://github.com/wimpomp/befunge.git` + +## Usage +`befunge --help` + +## Examples +`befunge examples/factorial0.bf -i 20 -d 0.05` \ No newline at end of file diff --git a/befunge/__init__.py b/befunge/__init__.py new file mode 100644 index 0000000..135a0b8 --- /dev/null +++ b/befunge/__init__.py @@ -0,0 +1,311 @@ +from enum import Enum +from random import randint +from argparse import ArgumentParser +from pathlib import Path +from time import sleep +from io import StringIO +from itertools import chain +from curses import wrapper + + +class OperatorException(Exception): + def __init__(self, op): + super().__init__(f'Could not parse operator {op}') + + +class Direction(Enum): + RIGHT = 0 + UP = 1 + LEFT = 2 + DOWN = 3 + + def __add__(self, other): + return Direction((self.value + other) % len(Direction)) + + def __radd__(self, other): + return self + other + + def __sub__(self, other): + return Direction((self.value - other) % len(Direction)) + + def __rsub__(self, other): + return self - other + + +class Input(list): + def __call__(self): + return self.pop(0) + + +class Grid(dict): + def __init__(self, code=None, version=None): + self.version = version or 'b93' + self.cursor = True + if self.version == 'b93': + self.shape = 80, 25 + else: + self.shape = None + self._ip = [0, 0] + self.direction = Direction.RIGHT + super().__init__() + for y, line in enumerate(code.splitlines()): + for x, char in enumerate(line): + self[(x, y)] = char + + def wrap(self, value, dim): + if self.shape is None: + return value + else: + return value % self.shape[dim] + + @property + def x(self): + return self._ip[0] + + @x.setter + def x(self, value): + self._ip[0] = self.wrap(value, 0) + + @property + def y(self): + return self._ip[1] + + @y.setter + def y(self, value): + self._ip[1] = self.wrap(value, 1) + + def __getitem__(self, key): + return self.get(key, ' ') + + def __setitem__(self, key, value): + super().__setitem__(tuple(self.wrap(k, i) for i, k in enumerate(key)), value) + + def __repr__(self): + lines = [] + for (x, y), value in self.items(): + while len(lines) <= y: + lines.append([]) + while len(lines[y]) <= x: + lines[y].append(' ') + lines[y][x] = value + lines = [''.join(line) for line in lines] + if self.cursor: + return '\n'.join(lines[:self.y] + + [lines[self.y][:self.x] + + '\x1b[37m\x1b[40m' + lines[self.y][self.x] + '\033[0m' + + lines[self.y][self.x + 1:]] + + lines[self.y + 1:]) + else: + return '\n'.join(lines) + + @property + def op(self): + return self[self.x, self.y] + + def advance(self): + match self.direction: + case Direction.RIGHT: + self.x += 1 + case Direction.UP: + self.y -= 1 + case Direction.LEFT: + self.x -= 1 + case Direction.DOWN: + self.y += 1 + + +class Stack(list): + def pop(self, index=-1): + try: + return super().pop(index) + except IndexError: + return 0 + + def push(self, value): + self.append(value) + + +class Befunge(Grid): + def __init__(self, code=None, version=None, inputs=None): + super().__init__(code, version) + self.output = None + if inputs is None: + self.input = input + else: + self.input = Input(inputs) + self.stack = Stack() + self.string = False + self.steps = 0 + self.terminated = False + self.operations = {'b93': '+-*/%!`><^v?_|":\\$.,#pg&~@ 1234567890'}[self.version] + + @staticmethod + def from_file(file, version=None, inputs=None): + file = Path(file) + if version is None: + match file.suffix: + case '.bf': + version = 'b93' + case suffix: + version = suffix.strip('.') + return Befunge(file.read_text(), version, inputs) + + def __repr__(self): + return f'grid:\n{super().__repr__()}\n\nstack:\n{self.stack}' + + def __iter__(self): + return self + + def __next__(self): + return self.step() + + def run(self): + for _ in self: + pass + + def debug(self, time_step): + def fun(stdscr): + def scr_input(): + height, width = stdscr.getmaxyx() + stdscr.move(height - 1, 0) + stdscr.clrtoeol() + stdscr.addstr(height - 1, 0, 'input?') + stdscr.move(self.y + 1, self.x) + stdscr.refresh() + return stdscr.getstr() + + self.output = StringIO() + self.cursor = False + if not isinstance(self.input, Input): + self.input = scr_input + stdscr.clear() + stdscr.refresh() + + for b in chain((self,), self): + height, width = stdscr.getmaxyx() + stdscr.clear() + stdscr.addstr(f'{b}\n\noutput:\n{b.output.getvalue()}\n\nstep:\n{b.steps}') + if time_step > 0: + stdscr.move(b.y + 1, b.x) + stdscr.refresh() + sleep(time_step) + else: + stdscr.addstr(height - 1, 0, 'Press any key to continue.') + stdscr.move(b.y + 1, b.x) + stdscr.refresh() + stdscr.getch() + + height, width = stdscr.getmaxyx() + stdscr.move(height - 1, 0) + stdscr.clrtoeol() + stdscr.addstr(height - 1, 0, 'Press any key to quit.') + stdscr.move(self.y + 1, self.x) + stdscr.getch() + + try: + wrapper(fun) + except KeyboardInterrupt: + pass + + def step(self, n=1): + if self.terminated: + raise StopIteration + m = 0 + while m < n: + if self.string: + if self.op == '"': + self.string = False + else: + self.stack.push(ord(self.op)) + elif self.op in self.operations: + match self.op: + case '+': + self.stack.push(self.stack.pop() + self.stack.pop()) + case '-': + self.stack.push(self.stack.pop(-2) - self.stack.pop()) + case '*': + self.stack.push(self.stack.pop() * self.stack.pop()) + case '/': + self.stack.push(self.stack.pop(-2) // self.stack.pop()) + case '%': + self.stack.push(self.stack.pop(-2) % self.stack.pop()) + case '!': + self.stack.push(int(not self.stack.pop())) + case '`': + self.stack.push(int(self.stack.pop() < self.stack.pop())) + case '>': + self.direction = Direction.RIGHT + case '<': + self.direction = Direction.LEFT + case '^': + self.direction = Direction.UP + case 'v': + self.direction = Direction.DOWN + case '?': + self.direction = Direction(randint(0, 3)) + case '_': + self.direction = Direction(2 * bool(self.stack.pop())) + case '|': + self.direction = Direction(3 - 2 * bool(self.stack.pop())) + case '[': + self.direction += 1 + case ']': + self.direction -= 1 + case '"': + self.string = True + case ':': + if len(self.stack): + self.stack.push(self.stack[-1]) + case '\\': + if len(self.stack) > 1: + self.stack.push(self.stack.pop(-2)) + case '$': + self.stack.pop() + case '.': + print(str(self.stack.pop()) + ' ', end='', file=self.output) + case ',': + print(chr(self.stack.pop()), end='', file=self.output) + case '#': + self.advance() + case 'p': + self[self.stack.pop(-2), self.stack.pop()] = chr(self.stack.pop()) + case 'g': + self.stack.push(ord(self[self.stack.pop(-2), self.stack.pop()])) + case '&': + self.stack.push(int(self.input())) + case '~': + self.stack.push(ord(self.input())) + case '@': + self.terminated = True + break + case ' ': + pass + case op: + self.stack.append(int(op)) + else: + raise OperatorException(self.op) + self.advance() + if not (not self.string and self.op == ' '): + m += 1 + self.steps += 1 + return self + + +def main(): + parser = ArgumentParser(description='Display info and save as tif') + group = parser.add_mutually_exclusive_group() + group.add_argument('file', help='funge code file', nargs='?') + group.add_argument('-s', '--string', help='funge code string', default=None) + parser.add_argument('-v', '--version', help='funge version: b93, b98', type=str, default=None) + parser.add_argument('-d', '--debug', help='debug, steps / second, 0: continue on key press', type=float, default=-1) + parser.add_argument('-i', '--inputs', help='inputs for when befunge asks for it', nargs='*') + args = parser.parse_args() + if args.file: + befunge = Befunge.from_file(args.file, args.version, args.inputs) + else: + befunge = Befunge(args.string, args.version, args.inputs) + + if args.debug < 0: + befunge.run() + else: + befunge.debug(args.debug) diff --git a/examples/ex1.bf b/examples/ex1.bf new file mode 100644 index 0000000..c5568e2 --- /dev/null +++ b/examples/ex1.bf @@ -0,0 +1,3 @@ + v < +>?"/",^ + >"\",^ diff --git a/examples/factorial.bf b/examples/factorial.bf new file mode 100644 index 0000000..dc2ad20 --- /dev/null +++ b/examples/factorial.bf @@ -0,0 +1,8 @@ +" ?tupni",v +v.:&,,,,,,< +>" = !",,,v +v1: <\0< ,< +> -:| +>* v$ +|:\<< +>$.25*,@ \ No newline at end of file diff --git a/examples/factorial0.bf b/examples/factorial0.bf new file mode 100644 index 0000000..e2ea4a9 --- /dev/null +++ b/examples/factorial0.bf @@ -0,0 +1,10 @@ +v +0 +& +>>:1v + |:-< + $ +>v + \ +*: +^_$.55+,@ \ No newline at end of file diff --git a/examples/hello_world.bf b/examples/hello_world.bf new file mode 100644 index 0000000..7ccf6d1 --- /dev/null +++ b/examples/hello_world.bf @@ -0,0 +1,5 @@ +> v +v ,,,,,"Hello"< +>48*, v +v,,,,,,"World!"< +>25*,@ \ No newline at end of file diff --git a/examples/hello_world2.bf b/examples/hello_world2.bf new file mode 100644 index 0000000..2c3185c --- /dev/null +++ b/examples/hello_world2.bf @@ -0,0 +1,3 @@ + >25*"!dlrow ,olleH":v + v:,_@ + > ^ diff --git a/examples/multiplier.bf b/examples/multiplier.bf new file mode 100644 index 0000000..61112ca --- /dev/null +++ b/examples/multiplier.bf @@ -0,0 +1 @@ +&&*.25*,@ \ No newline at end of file diff --git a/examples/random.bf b/examples/random.bf new file mode 100644 index 0000000..4ea14af --- /dev/null +++ b/examples/random.bf @@ -0,0 +1,8 @@ + v>>>>>v + 12345 + ^?^ + > ? ?^ + v?v + 6789 + >>>> v + ^ .< diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..62c0274 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +import os +import setuptools + +version = '2022.11.0' + +with open('README.md', 'r') as fh: + long_description = fh.read() + +with open(os.path.join(os.path.dirname(__file__), 'befunge', '_version.py'), 'w') as f: + f.write(f"__version__ = '{version}'\n") + try: + with open(os.path.join(os.path.dirname(__file__), '.git', 'HEAD')) as g: + head = g.read().split(':')[1].strip() + with open(os.path.join(os.path.dirname(__file__), '.git', head)) as h: + f.write("__git_commit_hash__ = '{}'\n".format(h.read().rstrip('\n'))) + except Exception: + f.write(f"__git_commit_hash__ = 'unknown'\n") + +setuptools.setup( + name='befunge', + version=version, + author='Wim Pomp', + author_email='wimpomp@gmail.com', + description='Befunge interpreter and debugger.', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/wimpomp/befunge', + packages=setuptools.find_packages(), + classifiers=[ + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Operating System :: OS Independent', + ], + python_requires='>=3.10', + install_requires=[], + entry_points={'console_scripts': ['befunge=befunge:main']} +)