Messenger/Python interpreter

From Esolang
Jump to navigation Jump to search

Main page: Messenger

###############################################################################
## MESSENGER - CREATED BY squareroot12621.                                   ##
###############################################################################
## MESSENGER DOCUMENTATION                                                   ##

# Code is treated as a 2D grid.
# Variables, literals, etc. are replaced with messages.
# Messages take 1 unit of time to go 1 character horizontally or vertically.
# A message can hold either a NULL, INT, or LIST.
#
# Each non-space character is a function.
# They take 1 or 2 arguments, and return 1 or 2 more messages.
# Note that for a 2-argument function, the argument order is determined by when
# they enter the function. If they come at the same time, an error is thrown.
#
# If a message escapes to the bottom edge, it gets printed.
# If it escapes to the right edge, it becomes a byte/bytestream and printed.
# If it escapes to the top or left edge, it disappears.
# Note that only one message may escape to the bottom or right edge at a time.
# If two or more do this at the same time, an error is thrown.
#
# FUNCTION | DESCRIPTION
# ---------+-------------------------------------------------------------------
#     <    | Redirects message to the left (absolute)
#     >    | Redirects message to the right (absolute)
#     ^    | Redirects message to the top (absolute)
#     v    | Redirects message to the bottom (absolute)
#  [space] | No-op
#     S    | Redirects message to left and right (relative)
#     N    | Message content = NULL
#  0 to 9  | Message content = INT of number
#     L    | Message content = LIST containing message content
#     I    | Message content = input (type is the same as when it came in)
#     +    | Return msg1 + msg2
#     -    | Return msg1 - msg2 (INT only)
#     *    | Return msg1 * msg2 (INT only)
#     /    | Return msg1 // msg2, or NULL if msg2 is 0 (INT only)
#     W    | Sends message to left (relative) if positive INT or LIST
#          |   Sends message to right (relative) if nonpositive INT or NULL
#     =    | Return 1 if msg1 == msg2, otherwise 0
#     G    | Return 1 if msg1 > msg2 (always LIST > INT > NULL), otherwise 0
#     B    | Send msg[:1] to left (relative), msg[1:] to right (relative)
#     E    | Send msg[:-1] to left (relative), msg[-1:] to right (relative)
#     R    | Send message left (relative) 50% of the time,
#          |   send message right (relative) 50% of the time
#     T    | Message content = Unix time in milliseconds (INT)
#
# The program runs by placing a NULL message in the top-left corner, (0, 0).
# If the character there isn't <, >, ^, or v, throw an error.


###############################################################################
## IMPORTS                                                                   ##

import argparse
import random
import time


###############################################################################
## MESSENGERMESSAGE CLASS                                                    ##

class MessengerMessage:
    """A message in Messenger."""

    def __init__(self, x, y, direction, content, grid, inFunc=True):
        """Return a MessengerMessage with the corresponding position,
        direction, and content, as well as the grid the message is in.
        """
        self.x = x
        self.y = y
        self.dir = direction
        self.content = content
        self.grid = grid
        self.inFunc = inFunc
        self.movedThisTick = False
        self.needsInput = False

    def __repr__(self):
        """Return a string representation of the MessengerMessage."""
        if self.inFunc:
            direction = 'inside a function'
        else:
            direction = f'going {self.dir}'
        return (f'<Message containing {self.content} at ({self.x}, {self.y}) '
                f'{direction}>')

    @property
    def type(self):
        """Return one of 'NULL', 'INT', or 'LIST', depending on the
        content attribute.
        """
        if self.content is None:
            return 'NULL'
        elif isinstance(self.content, int):
            return 'INT'
        elif isinstance(self.content, list):
            return 'LIST'
        else:
            raise TypeError(f'Message content is of an invalid type '
                            f'({repr(self.content)})')
        
    def clone(self):
        """Return a deep copy of the MessengerMessage."""
        messageClone = MessengerMessage(self.x, self.y, self.dir, self.content,
                                        self.grid, self.inFunc)
        messageClone.movedThisTick = self.movedThisTick
        messageClone.needsInput = self.needsInput
        return messageClone

    def tick(self):
        """Move the MessengerMessage and update the self.dir,
        self.content, self.inFunc, and self.movedThisTick attributes.
        """
        self.movedThisTick = not self.inFunc
        if not self.inFunc: # It can't move if it's stuck in a function
            # Move the message 1 unit
            if self.dir == 'up':
                self.y -= 1
            elif self.dir == 'down':
                self.y += 1
            elif self.dir == 'left':
                self.x -= 1
            elif self.dir == 'right':
                self.x += 1
            # Check if the message landed on a function
            characterAtPosition = self.grid[self.x, self.y]
            if characterAtPosition == ' ':
                self.inFunc = False
            elif characterAtPosition == '^':
                self.dir = 'up'
                self.inFunc = False
            elif characterAtPosition == '>':
                self.dir = 'right'
                self.inFunc = False
            elif characterAtPosition == 'v':
                self.dir = 'down'
                self.inFunc = False
            elif characterAtPosition == '<':
                self.dir = 'left'
                self.inFunc = False
            elif characterAtPosition == 'R':
                self.turn(random.choice([-1, 1]))
                self.inFunc = False
            elif characterAtPosition == 'W':
                if (self.type == 'LIST'
                    or (self.type == 'INT' and self.content > 0)):
                    self.turn(-1)
                elif (self.type == 'NULL'
                      or (self.type == 'INT' and self.content <= 0)):
                    self.turn(1)
                self.inFunc = False
            elif characterAtPosition == 'N':
                self.content = None
                self.inFunc = False
            elif characterAtPosition in '0123456789':
                self.content = int(characterAtPosition)
                self.inFunc = False
            elif characterAtPosition == 'L':
                self.content = [self.content]
                self.inFunc = False
            elif characterAtPosition == 'T':
                self.content = int(time.time() * 1000)
                self.inFunc = False
            elif characterAtPosition == 'I':
                self.needsInput = True
                self.inFunc = False
            else:
                self.inFunc = True

    def turn(self, rotations):
        """Change the direction of the MessengerMessage.
        Positive numbers indicate clockwise rotations, while
        negative numbers indicate counterclockwise rotations.
        """
        directionNumber = ['up', 'right', 'down', 'left'].index(self.dir)
        directionNumber = (directionNumber + rotations) % 4
        self.dir = ['up', 'right', 'down', 'left'][directionNumber]
        
    def release(self):
        """Set the inFunc attribute to False and return None."""
        self.inFunc = False


###############################################################################
## MESSENGERGRID CLASS                                                       ##
        
class MessengerGrid:
    """A 2D grid containing Messenger code."""

    def __init__(self, gridString):
        """Return a MessengerGrid formed from the code gridString."""
        # Raise an error if unknown characters are in code
        if (unknownCharSet := set(gridString)
            - set('<>^v SN0123456789LI+-*/W=GBERT\r\n')) != set():
            unknownCharList = sorted(list(unknownCharSet))
            characterPlural = 's' if len(unknownCharList) >= 2 else ''
            unknownCharStr = ', '.join([repr(i) for i in unknownCharList])
            raise SyntaxError(f'Unknown character{characterPlural} '
                              f'({unknownCharStr}) found in code')

        # Align the code into a grid format
        allLines = gridString.splitlines()
        if allLines == []: # Need at least 1 line
            allLines = ['']
        maxLineLength = max(map(len, allLines), default=1)
        self.code = [[char for char in line.ljust(maxLineLength)]
                     for line in allLines]

        # Get some information about the code grid
        self.width = maxLineLength
        self.height = len(allLines)
        self.originalCode = [[char for char in row] for row in self.code]
        
        # Check if the top-left corner is a redirector--
        # otherwise the message is stuck and throws an error
        if self[0, 0] in '<>^v':
            arrowsToWords = {'^': 'up',
                             '>': 'right',
                             'v': 'down',
                             '<': 'left'}
            self.messages = [MessengerMessage(0, 0, arrowsToWords[self[0, 0]],
                                              None, self, False)]
        else:
            raise ValueError(f'Top-left corner of code '
                             f'({repr(self.code[0][0])}) must be a redirector')

    def __repr__(self):
        """Return a string representation of the MessengerGrid."""
        grid = [[char for char in row] for row in self.code]
        for m in self.messages:
            grid[m.y][m.x] = '.'
        return '\n'.join([' '.join(row) for row in grid])

    def __getitem__(self, pos):
        """Return MessengerGrid.code[y][x], or ' ' if out of bounds."""
        x, y = pos
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.code[y][x]
        else:
            return ' '
    
    def tick(self):
        """Run 1 tick of Messenger code."""
        # Move all the messages
        for m in self.messages:
            m.tick()

        # Get input if necessary
        messagesNeedingInput = [m for m in self.messages if m.needsInput]
        if len(messagesNeedingInput) == 1: # Only one input
            previousContent = messagesNeedingInput[0].content
            if previousContent is None:
                raise TypeError("Can't input as type NULL")
            elif isinstance(previousContent, int):
                rawInput = input('Input an INT: ')
                try:
                    messagesNeedingInput[0].content = int(rawInput)
                    messagesNeedingInput[0].needsInput = False
                except ValueError:
                    raise ValueError(f'Invalid INT '
                                     f'({repr(rawInput)})') from None
            elif isinstance(previousContent, list):
                rawInput = input('Input a LIST: ').upper()
                if validList(rawInput):
                    messagesNeedingInput[0].content = rawInput
                    messagesNeedingInput[0].needsInput = False
                else:
                    raise ValueError(f'Invalid LIST ({repr(rawInput)})')
        elif len(messagesNeedingInput) > 1: # Too many inputs
            raise RuntimeError(f"{len(messagesNeedingInput)} inputs can't "
                               f'happen at the same time')

        # Print and delete messages that escaped from the program
        messagesToPrint = [m for m in self.messages
                           if m.x >= self.width or m.y >= self.height]
        if len(messagesToPrint) > 1:
            raise RuntimeError(f"{len(messagesToPrint)} messages can't be "
                               f'printed at the same time')
        messagesInBounds = []
        for m in self.messages:
            if m.x < 0 or m.y < 0: # Top or left edge
                pass
            elif m.x >= self.width: # Right edge
                print(str(m.content).replace('None', 'NULL'), end='')
            elif m.y >= self.height: # Bottom edge
                if m.type == 'INT':
                    print(chr(m.content), end='')
                elif m.type == 'LIST':
                    try:
                        print(''.join(map(chr, m.content)), end='')
                    except TypeError:
                        raise TypeError(f"Nested lists ({m.content}) can't be "
                                        f'converted to strings') from None
            else: # In bounds
                messagesInBounds.append(m)
        self.messages = messagesInBounds

        # Get rid of messages in the same position
        alreadySeen = {}
        for m in self.messages:
            if (m.x, m.y) in alreadySeen:
                if (functionAtPosition := self[m.x, m.y]) != ' ':
                    if (alreadySeen[(m.x, m.y)].movedThisTick
                        == m.movedThisTick):
                        # Two arguments to a function at the same time
                        raise RuntimeError("Two messages can't go into a "
                                           'function at the same time')
                    else: # One of +-*/=G is invoked
                        if m.movedThisTick:
                            firstArgument = alreadySeen[(m.x, m.y)]
                            secondArgument = m
                        else:
                            firstArgument = m
                            secondArgument = alreadySeen[(m.x, m.y)]
                        output = eval_bin_func(firstArgument,
                                               functionAtPosition,
                                               secondArgument)
                        secondArgument.content = output
                        secondArgument.release()
                        alreadySeen[(m.x, m.y)] = secondArgument
                else:
                    alreadySeen.pop((m.x, m.y))
            else:
                alreadySeen[(m.x, m.y)] = m
        self.messages = list(alreadySeen.values())
        
        # Run B, E, and S (splitters)
        updatedMessages = []
        for m in [message.clone() for message in self.messages]:
            if self[m.x, m.y] == 'S': # Split
                # Left clone
                updatedMessages.append(m.clone())
                updatedMessages[-1].turn(-1)
                updatedMessages[-1].release()
                # Right clone
                updatedMessages.append(m.clone())
                updatedMessages[-1].turn(1)
                updatedMessages[-1].release()
            elif self[m.x, m.y] == 'B': # Beginning
                if m.type == 'LIST':
                    # Left clone: (...)[0]
                    updatedMessages.append(m.clone())
                    updatedMessages[-1].turn(-1)
                    updatedMessages[-1].release()
                    if updatedMessages[-1].content == []:
                        newContent = None
                    else:
                        newContent = updatedMessages[-1].content[0]
                    updatedMessages[-1].content = newContent
                    # Right clone: (...)[1:]
                    updatedMessages.append(m.clone())
                    updatedMessages[-1].turn(1)
                    updatedMessages[-1].release()
                    newContent = updatedMessages[-1].content[1:]
                    updatedMessages[-1].content = newContent
                else:
                    raise TypeError(f"Can't calculate {m.type} B")
            elif self[m.x, m.y] == 'E': # End
                if m.type == 'LIST':
                    # Left clone: (...)[-1]
                    updatedMessages.append(m.clone())
                    updatedMessages[-1].turn(-1)
                    updatedMessages[-1].release()
                    if updatedMessages[-1].content == []:
                        newContent = None
                    else:
                        newContent = updatedMessages[-1].content[-1]
                    updatedMessages[-1].content = newContent
                    # Right clone: (...)[:-1]
                    updatedMessages.append(m.clone())
                    updatedMessages[-1].turn(1)
                    updatedMessages[-1].release()
                    newContent = updatedMessages[-1].content[:-1]
                    updatedMessages[-1].content = newContent
                else:
                    raise TypeError(f"Can't calculate {m.type} E")
            else:
                updatedMessages.append(m.clone())
        self.messages = updatedMessages

    def run(self, maxIterations):
        """Runs Messenger code until all messages either disappear or
        get trapped in functions."""
        tickNumber = 0
        if maxIterations == 0: # Don't check for tickNumber
            while not all([m.inFunc for m in self.messages]):
                self.tick()
                tickNumber += 1
        else:
            while not all([m.inFunc for m in self.messages]):
                self.tick()
                tickNumber += 1
                if tickNumber >= maxIterations:
                    raise RuntimeError(f'Ran for {tickNumber} iterations '
                                       f'without terminating')

    def reset(self):
        """Resets the Messenger code to before it was run."""
        self.code = [[char for char in row] for row in self.originalCode]
        arrowsToWords = {'^': 'up',
                         '>': 'right',
                         'v': 'down',
                         '<': 'left'}
        self.messages = [MessengerMessage(0, 0, arrowsToWords[self[0, 0]],
                                          None, self, False)]


###############################################################################
## OTHER FUNCTIONS                                                           ##
        
def validList(inputString):
    """Return True if inputString is a valid list in Messenger,
    and False otherwise.
    """
    if (not rawInput.startswith('[') or not rawInput.endswith(']')
        or rawInput.count('[') != rawInput.count(']')):
        # Doesn't have balanced or surrounding brackets
        return False
    else:
        listElements = rawInput[1:-1]
        if listElements == ['']: # Empty list
            return True
        for element in listElements: # Check each element
            if element == '': # Missing element
                return False
            if element == 'NULL': # NULL
                continue
            elif (element[0] in '-0123456789'
                  and set(element[1:]) <= set('0123456789')): # INT
                continue
            elif validList(element): # LIST
                continue
        return True
    
def eval_bin_func(message1, operator, message2):
    """Return the output content from applying operator on message1 and
    message2.
    """
    if operator == '+': # Add
        if message1.type == 'NULL' or message2.type == 'NULL':
            return None
        elif message1.type == message2.type == 'INT':
            return message1.content + message2.content
        elif message1.type == message2.type == 'LIST':
            return message1.content + message2.content
        elif message1.type == 'INT' and message2.type == 'LIST':
            return [message1.content] + message2.content
        elif message1.type == 'LIST' and message2.type == 'INT':
            return message1.content + [message2.content]
    elif operator == '-': # Subtract
        if message1.type == 'NULL' or message2.type == 'NULL':
            return None
        if message1.type == message2.type == 'INT':
            return message1.content - message2.content
        else:
            raise TypeError(f"Can't calculate"
                            f'{message1.type} - {message2.type}')
    elif operator == '*': # Multiply
        if message1.type == 'NULL' or message2.type == 'NULL':
            return None
        if message1.type == message2.type == 'INT':
            return message1.content * message2.content
        else:
            raise TypeError(f"Can't calculate"
                            f'{message1.type} * {message2.type}')
    elif operator == '/': # Divide
        if message1.type == 'NULL' or message2.type == 'NULL':
            return None
        if message1.type == message2.type == 'INT':
            try:
                return message1.content // message2.content
            except ZeroDivisionError:
                return None
        else:
            raise TypeError(f"Can't calculate"
                            f'{message1.type} / {message2.type}')
    elif operator == '=': # Equals
        return int(message1.content == message2.content)
    elif operator == 'G': # Greater than
        if ((message2.type == 'LIST' and message2.type in ['NULL', 'INT'])
            or (message1.type == 'INT' and message2.type == 'NULL')):
            return 1
        elif (message1.type == 'NULL'
              or (message1.type == 'INT' and message2.type == 'LIST')):
            return 0
        elif message1.type == message2.type == 'INT':
            return int(message1.content > message2.content)
        elif message1.type == message2.type == 'LIST':
            return int(message1.content > message2.content)
    else:
        raise RuntimeError(f"{operator} doesn't take 2 arguments")


###############################################################################
## WHILE RUNNING                                                             ##

if __name__ == '__main__': # Run directly
    formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=30)
    parser = argparse.ArgumentParser(
        prog='Messenger Interpreter v1.0.2',
        description='Runs Messenger, a 2D programming language designed to be '
                    'as annoying as possible.',
        formatter_class=formatter
    )
    parser.add_argument('code',
                        help='The Messenger code to be run.',
                        type=str)
    parser.add_argument('-c', '--check',
                        help='show grid and flags before running code',
                        action='store_true')
    parser.add_argument('-i', '--iterations',
                        help='runs for ITER iterations; 0 = runs forever '
                             '(DEFAULT: 50000)',
                        metavar='ITER',
                        type=int,
                        default=50000)
    arguments = parser.parse_args()
    
    grid = MessengerGrid(arguments.code)
    if arguments.check:
        print(f'\nGrid:\n{grid}\n')
        print(f'Arguments:\n'
              f'-i, --iterations: {arguments.iterations}'
              f'\n')
        gridChecked = input('Type "no" (without quotes) to cancel execution.\n'
                            'Type anything else to continue.\n')
        if gridChecked.upper() != 'NO':
            grid.run(arguments.iterations)
    else:
        grid.run(arguments.iterations)