Simple interpreter for a golfing language

Posted on

Problem

This is the seconds version of an interpreter yesterday. It’s supposed to be used for code golf, but it currently can’t do much.

import re, functools, time

stack = [""] * 9

def run(file):
    f = open(file, 'r')
    code = f.read()
    f.close()
    parse(code)

def prompt():
    """Lets the user input commands and feeds them to parse()"""
    while True:
        userInput = input("> ")
        if len(userInput) > 0:
            return userInput
        print("Please enter a command.")

def parse_numbers(userInput, operator):
    """Parses math like 100+100"""
    numbers = userInput.split(operator)
    try:
        return map(int, numbers)
    except ValueError as error:
        print(error.message)

def add(command):
    """Adds two or more numbers together"""
    numbers = parse_numbers(command, '+')
    return sum(numbers)

def subtract(command):
    """Subtracts two or more numbers"""
    numbers = parse_numbers(command, '-')
    return functools.reduce(lambda x, y: x - y, numbers)

def multiply(command):
    """Multiplies two or more numbers"""
    numbers = parse_numbers(command, '*')
    return functools.reduce(lambda x, y: x * y, numbers)

def divide(command):
    """Divides two or more numbers"""
    numbers = parse_numbers(command, '/')
    return functools.reduce(lambda x, y: x / y, numbers)

def modulus(command):
    """Prints the remainder of two numbers"""
    numbers = parse_numbers(command, '%')
    return functools.reduce(lambda x, y: x % y, numbers)

def quine(command):
    """Prints itself"""
    print(command)

def print_command(command):
    """Prints whatever is after the command"""
    print(command[2:])

def var(command):
    command = command.split('=')
    command[0].replace(" ", "")
    command[1].replace(" ", "")
    stack[command[0]] = command[1]

def loop(command, amount):
    """Loops (a) command(s) for multiple times"""
    for i in range(0, int(amount)):
        parse(command)

def putOnStack(text, num):
    stack[int(num)] = text

def parse(command):
    """Parses the commands."""
    if ';' in command:
        commands = command.split(";")
        for i in commands:
            parse(i)
    if 'n' in command:
        commands = command.split('n')
        for i in commands:
            parse(i)
    elif command.startswith("q"):
        quine(command)
    elif command.startswith("p "):
        print_command(command)
    elif command.startswith("l "):
        try:
            loopAmount = re.sub("D", "", command)
            lst = command.split(loopAmount)
            strCommand = lst[1]
            strCommand = strCommand[1:]
            loop(strCommand, loopAmount)
        except IndexError as error:
            print("Error: Can't put numbers in loop")
    elif '+' in command:
        print (add(command))
    elif '-' in command:
        print (subtract(command))
    elif '*' in command:
        print (multiply(command))
    elif '/' in command:
        print (divide(command))
    elif '%' in command:
        print (modulus(command))
    elif '=' in command:
        lst = command.split('=')
        lst[0].replace(" ", "")
        lst[1].replace(" ", "")
        stackNum = ''.join(lst[1])
        putOnStack(stackNum, lst[0])
    elif command.startswith("run "):
        command = command.replace(" ", "")
        command = command.split("run")
        run(command[1])
    elif command.startswith('#'):
        pass
    elif command.startswith('? '):
        stackNum = command[2]
        text = input("input> ")
        putOnStack(text, stackNum)
    elif command.startswith('@ '):
        stackNum = command[2]
        print(''.join(stack[int(stackNum)]))
    elif command.startswith("."):
        time.sleep(2)
    else:
        print("Invalid command")

while True:
    userInput = prompt()
    parse(userInput)

Here’s a few examples of programs using it:

p What's your name?;? 1;p Hello,;@ 1
l 5 p Hello, world!

Commands:


  • p (text): Prints text
  • q (text): Creates a quine
  • l (x) (command): Loops over commands
  • +, -, *, /**: Arithmetic operators
  • ? (x): Asks for user input and puts it on stack
  • @ (x): Reads from stack
  • .: Sleeps for 2 seconds
  • # (text): A simple comment

There’s a few bad things about this, namely:

  • You can’t use numbers in loops. Check the loop() function to see why.

  • You can’t use variables in print statements, math, etc.

  • You can’t get p to print on one line instead of adding newlines after the text.

So, what do you guys think about it?

Solution

Assuming Python 3: [Edit: Joe Wallis identifies it as Python2; I think my comments all stand, but I’d add recommending not using file as a variable name, because file() is a builtin function].

    f = open(file, 'r')
    code = f.read()
    f.close()
    parse(code)

This “open/read/close” pattern is more idiomatic as:

with open(file, 'r') as f:
    code = f.read()

This

    if len(userInput) > 0:
        return userInput

You can test strings directly, if they are empty they are boolean false, otherwise true, so it can become:

    if userInput:
        return userInput

These:

def subtract(command):
    """Subtracts two or more numbers"""
    numbers = parse_numbers(command, '-')
    return functools.reduce(lambda x, y: x - y, numbers)

def multiply(command):

def divide(command):

def modulus(command):

could all use the operator module, which has standard operators (+,-,*,/,%) as functions. And then you could remove all your lambdas, e.g.:

import operator
def subtract(command):
    """Subtracts two or more numbers"""
    numbers = parse_numbers(command, '-')
    return functools.reduce(operator.sub, numbers)

(NB. noted by Joe Wallis, Python 3 has different division methods in its operator module, truediv() and floordiv() instead of just div(). Might be relevant because your use of functools and print() might mean you’re going for Python 3-compatible code).

And then once you have four things which just do reduce(operator.___, numbers) you could merge those into one function, possibly with a hashtable like {'+':operator.add, '-':operator.sub,...} or similar.

Then you could shorten the elif/elif/elif block later when calling them.


def print_command(command):
    """Prints whatever is after the command"""
    print(command[2:])

This isn’t terrible, but having 2 as a magic number in the code isn’t very clear, and it hard-codes a possible instruction length. And this function naming and comment show that you’re using ‘command’ to mean both instructions your interpreter has, and the text of instruction with arguments supplied by the user. That’s needlessly mixed up.

It would be clearer to change the names of things so it wasn’t mixed up, and have some way to .lstrip the instruction name off the parameters, or otherwise separate the instruction as a whole item, without relying on it being 2 characters, e.g. split on the first space character.


def var(command):
    command = command.split('=')
    command[0].replace(" ", "")
    command[1].replace(" ", "")
    stack[command[0]] = command[1]

Why not replace the spaces before splitting it? Then you don’t need to do it twice. If it’s always going to split into two parts, you could make it:

def var(command):
    x, y = command.replace(' ', '').split('=')
    stack[x] = y

def putOnStack(text, num):
    stack[int(num)] = text

I haven’t followed where this is called from, but it looks like it doesn’t put On the stack, it puts In the stack. (And if so, is it really a stack at all?)


def parse(command):
    """Parses the commands."""
    if ';' in command:
        commands = command.split(";")
        for i in commands:
            parse(i)
    if 'n' in command:
        commands = command.split('n')
        for i in commands:
            parse(i)

These could be one block, e.g.:

    for c in (';', 'n'):
        if c in command:
            commands = command.split(c)
            for i in commands:
                parse(i)

I’m not sure how well that will work, if a command has both in it, it will get processed twice. But then, it will in your code as well. It might be possible to use re.split('[;n]', command) to split on either character – maybe.


    elif '=' in command:
        lst = command.split('=')
        lst[0].replace(" ", "")
        lst[1].replace(" ", "")
        stackNum = ''.join(lst[1])
        putOnStack(stackNum, lst[0])

Similar to earlier, replace the spaces first. (Or maybe do it once at the top for the whole processing code).

You use ''.join(lst[1]) implying that lst[1] is a list, but it can’t be a list, it comes from split(), it’s always going to be a string. Should that be ''.join(lst[1:]) ?

[Edit: Now I have traced this through, this spurious join is why you have to call int(num) in putOnStack. It makes more sense to put the int() call here, rather than there.]


    elif command.startswith('? '):
        stackNum = command[2]
    [..]
    elif command.startswith('@ '):
        stackNum = command[2]

These might break if they put more than one space in between ? and the number.

Avoid crashes

The interactive shell shall not abruptly crush on wrong input, it makes experimental playing with your language a pain:

> @ (1)
Traceback (most recent call last):
  File "/home/riccardo/in.py", line 132, in <module>
    parse(prompt())
  File "/home/riccardo/in.py", line 125, in parse
    print(''.join(stack[int(stackNum)]))
ValueError: invalid literal for int() with base 10: '('

The session ends.

To continue using your program I should re-run the script losing much time. Using a try-except would make the output like:

> @ (1)
Error: ValueError: invalid literal for int() with base 10: '('
> 1 + 1
2

So after an error I can continue writing.

Use with

with automatically closes your files and it preferred over manual opening and closing.

def run(file):
    with open(file, 'r') as f:
        code = f.read()
    parse(code)

Remove all repetition

if ';' in command:
    commands = command.split(";")
    for i in commands:
        parse(i)
if 'n' in command:
    commands = command.split('n')
    for i in commands:
        parse(i)

You are doing the same thing twice, and what if I decide that <> [arbitrary delimiter] also delimits commands? This solution is not extensible.

I suggest using a simple for loop:

for delimiter in (';', 'n', '<>'):
    if delimiter in command:
        for sub_command in  command.split(delimiter):
            parse(sub_command)

text = input("input> ")

print("Error: Can't put numbers in loop")

If it’s intended for golfing, it probably shouldn’t print things it’s not asked to (or at least have non-verbose options for commands that do). Golfing challenges most of the time require very specific input/output.

Leave a Reply

Your email address will not be published. Required fields are marked *