diff --git a/esolang/__init__.py b/esolang/__init__.py new file mode 100644 index 0000000..10de2bf --- /dev/null +++ b/esolang/__init__.py @@ -0,0 +1,5 @@ +from .esolang import Esolang + + +def setup(bot): + bot.add_cog(Esolang(bot)) diff --git a/esolang/befunge.py b/esolang/befunge.py new file mode 100644 index 0000000..4ae7e4d --- /dev/null +++ b/esolang/befunge.py @@ -0,0 +1,312 @@ +from typing import List +from enum import Enum +import random +import io + + +# Utility +class Mode(Enum): + LITERAL = 1 + STRING = 2 + + +class Point: + def __init__(self): + self.x = 0 + self.y = 0 + + def move(self, direction: List[int]) -> None: + self.x += direction[0] + self.y += direction[1] + + +DIRECTIONS = [[1, 0], [0, 1], [-1, 0], [0, -1]] + + +# Exceptions +class EmptyStack(SyntaxError): + """Raised when the the stack is accessed in an empty state""" + + def __init__(self, message: str, codemap: List[List[str]], position: Point): + super().__init__( + message, + ("program.befunge", position.y + 1, position.x + 1, "".join(codemap[position.y])), + ) + + +class UnknownSymbol(SyntaxError): + def __init__(self, message: str, codemap: List[List[str]], position: Point): + super().__init__( + message, + ("program.befunge", position.y + 1, position.x + 1, "".join(codemap[position.y])), + ) + + +class NoTermination(SyntaxError): + def __init__(self, message: str, code: str): + if len(code) > 50: + code = code[-50:] + ptr = 50 + else: + ptr = len(code) + super().__init__( + message, ("program.befunge", 1, ptr, code), + ) + + +# Core classes + + +class Stack: + def __init__(self, codemap: List[List[str]], pointer: Point): + self._internal: List[int] = [] + self.codemap: List[List[str]] = codemap + self.pointer = pointer + + def push(self, value: int) -> None: + self._internal.append(value) + + def __pop(self) -> int: + return self._internal.pop() + + def pop(self) -> int: + try: + return self.__pop() + except IndexError: + raise EmptyStack("Empty stack in pop call", self.codemap, self.pointer) + + def addition(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in addition call", self.codemap, self.pointer) + self.push(a + b) + + def subtraction(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in subtraction call", self.codemap, self.pointer) + self.push(a - b) + + def multiplication(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in multiplication call", self.codemap, self.pointer) + self.push(a * b) + + def division(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in division call", self.codemap, self.pointer) + self.push(a // b) + + def modulo(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in modulo call", self.codemap, self.pointer) + self.push(a % b) + + def lnot(self) -> None: + try: + a = self.__pop() + except IndexError: + raise EmptyStack("Empty stack in logical not call", self.codemap, self.pointer) + self.push(1 if a == 0 else 0) + + def greater(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in logical greater call", self.codemap, self.pointer) + self.push(1 if b > a else 0) + + def underscore(self) -> List[int]: + try: + a = self.__pop() + except IndexError: + a = 0 + if a == 0: + return [1, 0] + else: + return [-1, 0] + + def pipe(self) -> List[int]: + try: + a = self.__pop() + except IndexError: + a = 0 + if a == 0: + return [0, 1] + else: + return [0, -1] + + def duplicate(self) -> None: + try: + a = self.__pop() + except IndexError: + a = 0 + self.push(a) + self.push(a) + + def swap(self) -> None: + try: + a = self.__pop() + except IndexError: + raise EmptyStack("Empty stack in swap call", self.codemap, self.pointer) + + try: + b = self.__pop() + except IndexError: + b = 0 + self.push(a) + self.push(b) + + +class Befunge: + @staticmethod + def check_syntax(code: str) -> None: + if "@" not in code: + raise NoTermination("Program does not terminate", code) + if code.count('"') % 2 != 0: + raise NoTermination("Program has an un-ending stringmode segment", code) + + @staticmethod + def buildcodemap(code: str) -> List[List[str]]: + codemap = code.split("\n") + max_segment = 0 + for index, segment in enumerate(codemap): + codemap[index] = [x for x in segment] + max_segment = max((max_segment, len(codemap[index]))) + + for i in range(len(codemap)): + try: + if len(codemap[i]) == 0: + del codemap[i] + except IndexError: + # We have to catch this here because we are modifying the list + # and so the indexes will become invalid + break + + for index, segment in enumerate(codemap): + if len(codemap[index]) < max_segment: + codemap[index] = codemap[index] + ([" "] * (max_segment - len(codemap[index]))) + return codemap + + @staticmethod + async def evaluate(code: str) -> io.StringIO: + Befunge.check_syntax(code) + + codemap: List[List[str]] = Befunge.buildcodemap(code) + pointer: Point = Point() + stack: Stack = Stack(codemap, pointer) + + mode: int = Mode.LITERAL + direction: List[int] = [1, 0] + output = io.StringIO("") + skip_next = False + counter: int = 0 + + while counter < 100000: + try: + assert pointer.y > -1 + row: List[str] = codemap[pointer.y] + except (IndexError, AssertionError): + if pointer.y == -1 and direction[1] == 1: + pointer.y += 1 + continue + elif pointer.y == -1 and direction[1] == -1: + pointer.y = len(codemap) - 1 + continue + elif pointer.y == len(codemap) and direction[1] == 1: + pointer.y = 0 + continue + elif pointer.y == len(codemap) and direction[1] == -1: + pointer.y = len(codemap) - 1 + continue + + try: + assert pointer.x > -1 + char: str = row[pointer.x] + except (IndexError, AssertionError): + if pointer.x == -1 and direction[0] == 1: + pointer.x += 1 + continue + elif pointer.x == -1 and direction[0] == -1: + pointer.x = len(row) - 1 + continue + elif pointer.x == len(row) and direction[0] == 1: + pointer.x = 0 + continue + elif pointer.x == len(row) and direction[0] == -1: + pointer.x = len(row) - 1 + continue + + if skip_next: + pointer.move(direction) + skip_next = False + continue + + if mode == Mode.LITERAL: + if char == ">": + direction = [1, 0] + elif char == "<": + direction = [-1, 0] + elif char == "^": + direction = [0, -1] + elif char == "v": + direction = [0, 1] + elif char == "?": + direction = random.choice(DIRECTIONS) + elif char == "+": + stack.addition() + elif char == "-": + stack.subtraction() + elif char == "*": + stack.multiplication() + elif char == "/": + stack.division() + elif char == "%": + stack.modulo() + elif char == "!": + stack.lnot() + elif char == "`": + stack.greater() + elif char == "_": + direction = stack.underscore() + elif char == "|": + direction = stack.pipe() + elif char == ":": + stack.duplicate() + elif char == "\\": + stack.swap() + elif char == "$": + stack.pop() + elif char == ".": + output.write(str(stack.pop())) + output.write(" ") + elif char == ",": + output.write(chr(stack.pop())) + elif char == "#": + skip_next = True + elif char == "@": + return output, stack._internal + elif char == '"': + mode = Mode.STRING + elif char == " ": + pass + elif char.isdigit(): + stack.push(int(char)) + else: + raise UnknownSymbol(char, codemap, pointer) + elif mode == Mode.STRING: + if char == '"': + mode = Mode.LITERAL + else: + stack.push(ord(char)) + pointer.move(direction) + counter += 1 + return output, stack._internal diff --git a/esolang/brainfuck.py b/esolang/brainfuck.py new file mode 100644 index 0000000..0a4622c --- /dev/null +++ b/esolang/brainfuck.py @@ -0,0 +1,66 @@ +import io + + +class Brainfuck: + @staticmethod + def cleanup(code): + return "".join(filter(lambda x: x in [".", "[", "]", "<", ">", "+", "-"], code)) + + @staticmethod + def getlines(code): + return [code[i : i + 50] for i in range(0, len(code), 50)] + + @staticmethod + def buildbracemap(code): + temp_bracestack, bracemap = [], {} + for position, command in enumerate(code): + if command == "[": + temp_bracestack.append(position) + elif command == "]": + start = temp_bracestack.pop() + bracemap[start] = position + bracemap[position] = start + return bracemap + + @staticmethod + def evaluate(code): + code = Brainfuck.cleanup(list(code)) + bracemap = Brainfuck.buildbracemap(code) + cells, codeptr, cellptr, prev = [0], 0, 0, -1 + + output = io.StringIO("") + + while codeptr < len(code): + command = code[codeptr] + if command == ">": + cellptr += 1 + if cellptr == len(cells): + cells.append(0) + elif command == "<": + cellptr = 0 if cellptr <= 0 else cellptr - 1 + elif command == "+": + cells[cellptr] = cells[cellptr] + 1 if cells[cellptr] < 255 else 0 + elif command == "-": + cells[cellptr] = cells[cellptr] - 1 if cells[cellptr] > 0 else 255 + elif command == "[": + if cells[cellptr] == 0: + codeptr = bracemap[codeptr] + else: + prev = cells[cellptr] + elif command == "]": + if cells[cellptr] == 0: + prev = 0 + else: + if cells[cellptr] == prev: + lines = Brainfuck.getlines("".join(code)) + errorptr = codeptr % 50 + raise SyntaxError( + f"Infinite loop: []", ("program.bf", len(lines), errorptr, lines[-1]) + ) + else: + codeptr = bracemap[codeptr] + elif command == ".": + output.write(chr(cells[cellptr])) + + codeptr += 1 + return output, cells diff --git a/esolang/cow.py b/esolang/cow.py new file mode 100644 index 0000000..7ea9249 --- /dev/null +++ b/esolang/cow.py @@ -0,0 +1,127 @@ +import io + + +class COW: + + instruction_mapping = { + 0: "moo", + 1: "mOo", + 2: "moO", + 3: "mOO", + 4: "MOo", + 5: "MoO", + 6: "MOO", + 7: "MOO", + 8: "OOO", + 9: "MMM", + 10: "OOM", + } + + @staticmethod + def cleanup(code): + return "".join(filter(lambda x: x in ["m", "o", "M", "O"], code)) + + @staticmethod + def getlines(code): + return [code[i : i + 50] for i in range(0, len(code), 50)] + + @staticmethod + def buildbracemap(code): + temp_bracestack, bracemap = [], {} + for position, command in enumerate(code): + if command == "MOO": + temp_bracestack.append(position) + elif command == "moo": + start = temp_bracestack.pop() + bracemap[start] = position + bracemap[position] = start + if temp_bracestack: + lines = COW.getlines("".join(code)) + raise SyntaxError( + "Trailing MOO", ("program.moo", len(lines), len(lines[-1]), lines[-1]) + ) + return bracemap + + @staticmethod + def evaluate(code): + code = COW.cleanup(code) + + if len(code) % 3 != 0: + lines = COW.getlines(code) + raise SyntaxError( + "Trailing command", ("program.moo", len(lines), len(lines[-1]), lines[-1]) + ) + code = [code[i : i + 3] for i in range(0, len(code), 3)] + + bracemap = COW.buildbracemap(code) + cells, codeptr, cellptr, prev, registry = [0], 0, 0, -1, -1 + output = io.StringIO("") + + while codeptr < len(code): + command = code[codeptr] + if command == "moO": + cellptr += 1 + if cellptr == len(cells): + cells.append(0) + elif command == "mOo": + cellptr = 0 if cellptr <= 0 else cellptr - 1 + elif command == "MoO": + cells[cellptr] = cells[cellptr] + 1 if cells[cellptr] < 255 else 0 + elif command == "MOo": + cells[cellptr] = cells[cellptr] - 1 if cells[cellptr] > 0 else 255 + elif command == "MOO": + if cells[cellptr] == 0: + codeptr = bracemap[codeptr] + else: + prev = cells[cellptr] + elif command == "moo": + if cells[cellptr] == 0: + prev = 0 + else: + if cells[cellptr] == prev: + lines = COW.getlines("".join(code)) + errorptr = ((codeptr * 3) % 50) + 3 + raise SyntaxError( + "Infinite loop: MOO/moo", + ("program.moo", len(lines), errorptr, lines[-1]), + ) + else: + codeptr = bracemap[codeptr] + elif command == "Moo": + output.write(chr(cells[cellptr])) + elif command == "mOO": + try: + code[codeptr] = COW.instruction_mapping[cells[cellptr]] + except KeyError: + lines = COW.getlines("".join(code)) + errorptr = ((codeptr * 3) % 50) + 3 + raise SyntaxError( + f"Invalid mOO execution in memory address {cellptr}: {cells[cellptr]}", + ("program.moo", len(lines), errorptr, lines[-1]), + ) + if code[codeptr] == "mOO": + lines = COW.getlines("".join(code)) + errorptr = ((codeptr * 3) % 50) + 3 + raise SyntaxError( + "Infinite loop: mOO", ("program.moo", len(lines), errorptr, lines[-1]) + ) + continue + elif command == "MMM": + if registry == -1: + registry = cells[cellptr] + else: + cells[cellptr] = registry + registry = -1 + elif command == "OOO": + cells[cellptr] = 0 + elif command == "OOM": + output.write(str(cells[cellptr])) + else: + lines = COW.getlines("".join(code)) + errorptr = ((codeptr * 3) % 50) + 3 + raise SyntaxError( + "Invalid COW command", ("program.moo", len(lines), errorptr, lines[-1]) + ) + + codeptr += 1 + return output, cells diff --git a/esolang/esolang.py b/esolang/esolang.py new file mode 100644 index 0000000..66924b1 --- /dev/null +++ b/esolang/esolang.py @@ -0,0 +1,128 @@ +from redbot.core import commands, checks +from redbot.core.utils.chat_formatting import box +import traceback +import functools +import asyncio +import inspect + +from .brainfuck import Brainfuck +from .cow import COW +from .befunge import Befunge +from .whitespace import Whitespace + + +class Esolang(commands.Cog): + """Do not ever look at the source for this""" + + def __init__(self, bot): + self.bot = bot + + @checks.is_owner() + @commands.command() + async def brainfuck(self, ctx, *, code): + """Run brainfuck code""" + try: + output, cells = Brainfuck.evaluate(code) + except Exception as error: + await ctx.send( + box("".join(traceback.format_exception_only(type(error), error)), lang="py") + ) + else: + output.seek(0) + output = output.read() + await ctx.send( + box( + f"[Memory]: {'[' + ']['.join(list(map(str, cells))) + ']'}\n" + f"[Output]: {output}", + lang="ini", + ) + ) + + @checks.is_owner() + @commands.command() + async def cow(self, ctx, *, code): + """Run COW code""" + try: + output, cells = COW.evaluate(code) + except Exception as error: + await ctx.send( + box("".join(traceback.format_exception_only(type(error), error)), lang="py") + ) + else: + output.seek(0) + output = output.read() + await ctx.send( + box( + f"[Memory]: {'[' + ']['.join(list(map(str, cells))) + ']'}\n" + f"[Output]: {output}", + lang="ini", + ) + ) + + @checks.is_owner() + @commands.command() + async def befunge(self, ctx, *, code): + """Run Befunge code""" + if code.startswith("```") and code.endswith("```"): + code = code[3:-3] + + try: + task = self.bot.loop.create_task(Befunge.evaluate(code)) + await asyncio.wait_for(task, timeout=5.0) + output, cells = task.result() + except asyncio.TimeoutError: + await ctx.send("Your befunge program took too long to run.") + except Exception as error: + frame_vars = inspect.trace()[-1][0].f_locals + stack = frame_vars.get("stack", frame_vars.get("self")) + if stack: + stack = "[" + "][".join(list(map(str, stack._internal[:10]))) + "]" + else: + stack = "Not initialized" + await ctx.send( + box( + f"[Stack]: {stack}\n" + f"[Exception]:\n{''.join(traceback.format_exception_only(type(error), error))}", + lang="ini", + ) + ) + else: + output.seek(0) + output = output.read() + await ctx.send( + box( + f"[Stack]: {'[' + ']['.join(list(map(str, cells))) + ']'}\n" + f"[Output]: {output}", + lang="ini", + ) + ) + + @checks.is_owner() + @commands.command() + async def whitespace(self, ctx, *, code): + """Run whitepsace code. + + Since Discord auto-converts tabs to spaces, use EM QUAD instead. + + If you need to copy it, here: `  +` + """ + try: + wrapped = functools.partial(Whitespace.evaluate, code=code) + future = self.bot.loop.run_in_executor(None, wrapped) + for x in range(500): + await asyncio.sleep(0.01) + try: + output = future.result() + except asyncio.InvalidStateError: + continue + else: + break + except Exception as error: + await ctx.send( + box("".join(traceback.format_exception_only(type(error), error)), lang="py") + ) + else: + output.seek(0) + output = output.read() + await ctx.send(box(f"[Output]: {output}", lang="ini",)) diff --git a/esolang/info.json b/esolang/info.json new file mode 100644 index 0000000..a970edc --- /dev/null +++ b/esolang/info.json @@ -0,0 +1,14 @@ +{ + "author": [ + "Neuro Assassin" + ], + "install_msg": "Due to the eval nature of this cog, this may cause issues in performance including blocking of bot. As a result, do not use in a production environment. Copyright holders and contributors hold no responsibility for damamges caused by usage of this code.", + "name": "esolang", + "short": "Run code in esoteric languages.", + "description": "Run code in some of the more popular esoteric languages for fun.", + "tags": [ + "fun" + ], + "requirements": [], + "hidden": true +} \ No newline at end of file diff --git a/esolang/whitespace.py b/esolang/whitespace.py new file mode 100644 index 0000000..cd64d22 --- /dev/null +++ b/esolang/whitespace.py @@ -0,0 +1,201 @@ +from typing import List +import io + + +# Exceptions +class EmptyStack(SyntaxError): + """Raised when the the stack is accessed in an empty state""" + + def __init__(self, message: str, code: str, pointer: int): + if pointer > 50: + line = pointer // 50 + start = line * 50 + code = code[start : start + 50] + pointer %= 50 + else: + line = 0 + super().__init__( + message, ("program.ws", line + 1, pointer + 1, code), + ) + + +class InvalidNumber(SyntaxError): + """Raised when a number fails to be parsed""" + + def __init__(self, message: str, code: str, pointer: int): + if pointer > 50: + line = pointer // 50 + start = line * 50 + code = code[start : start + 50] + pointer %= 50 + else: + line = 0 + super().__init__( + message, ("program.ws", line + 1, pointer + 1, code), + ) + + +# Core classes + + +class Stack: + def __init__(self, code: str): + self._internal: List[int] = [] + self.code = code + self.pointer: int = 0 + + def push(self, value: int) -> None: + self._internal.append(value) + + def __pop(self) -> int: + return self._internal.pop() + + def pop(self) -> int: + try: + return self.__pop() + except IndexError: + raise EmptyStack("Empty stack in pop call", self.code, self.pointer) + + def duplicate(self) -> None: + try: + a = self.__pop() + except IndexError: + a = 0 + self.push(a) + self.push(a) + + def swap(self) -> None: + try: + a = self.__pop() + except IndexError: + raise EmptyStack("Empty stack in swap call", self.code, self.pointer) + + try: + b = self.__pop() + except IndexError: + b = 0 + self.push(a) + self.push(b) + + def addition(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in addition call", self.codemap, self.pointer) + self.push(a + b) + + def subtraction(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in subtraction call", self.codemap, self.pointer) + self.push(a - b) + + def multiplication(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in multiplication call", self.codemap, self.pointer) + self.push(a * b) + + def division(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in division call", self.codemap, self.pointer) + self.push(a // b) + + def modulo(self) -> None: + try: + a, b = self.__pop(), self.__pop() + except IndexError: + raise EmptyStack("Empty stack in modulo call", self.codemap, self.pointer) + self.push(a % b) + + +class Whitespace: + @staticmethod + def clean_syntax(code: str) -> str: + if code.startswith("```\n"): + code = code[4:] + if code.endswith("\n```"): + code = code[:-4] + code = "".join(filter(lambda x: x in ["\u2001", " ", "\n"], code)) + code = code.replace("\u2001", "t").replace(" ", "s").replace("\n", "l") + return code + + @staticmethod + def parse_to_number(stack: Stack, number: str) -> int: + try: + sign = 1 if number[0] == "s" else -1 + + number = number[1:].replace("s", "0").replace("t", "1") + number = int(number, 2) + return number * sign + except IndexError: + raise InvalidNumber( + "Incorrect number: must be at least two chars", stack.code, stack.pointer + ) + + @staticmethod + def evaluate(code: str) -> io.StringIO: + code: str = Whitespace.clean_syntax(code) + stack: Stack = Stack(code) + command: str = "" + param: str = "" + + output: io.StringIO = io.StringIO("") + + while stack.pointer <= len(code): + print(command) + print(stack._internal) + try: + target = code[stack.pointer] + except IndexError: + target = "" + if command == "ss": + if target == "l": + number = Whitespace.parse_to_number(stack, param) + stack.push(number) + command, param = "", "" + elif target: + param += target + else: + raise InvalidNumber( + "Incorrect stack push: No argument supplied", stack.code, stack.pointer + ) + stack.pointer += 1 + continue + elif command == "sls": + stack.duplicate() + command, param = "", "" + elif command == "slt": + stack.swap() + command, param = "", "" + elif command == "sll": + stack.pop() + command, param = "", "" + elif command == "tsss": + stack.addition() + command, param = "", "" + elif command == "tsst": + stack.subtraction() + command, param = "", "" + elif command == "tssl": + stack.multiplication() + command, param = "", "" + elif command == "tsts": + stack.division() + command, param = "", "" + elif command == "tstt": + stack.modulo() + command, param = "", "" + elif command == "tlss": + output.write(chr(stack.pop())) + command, param = "", "" + elif command == "tlst": + output.write(str(stack.pop())) + command, param = "", "" + command += target + stack.pointer += 1 + return output