12
\$\begingroup\$

I've made an interpreted language that's like Brainfuck except it has keywords instead of characters. Here's an explanation of the commands, and how to run it.

  • add_ostream - Add the ASCII value of the current cell to the output stream.
  • chg_size & [position]; - Change the current position on the stack.
  • reset_stack; - Reset the stack and all its cells to the defaults.
  • chg_size & [size]; - Change the size of the stack. This resets all cell values.
  • reset_pos; - Reset the current position on the stack.
  • chg_cell & [value]; - Change the value of the current cell.
  • out_stream; - Output the output stream.
  • reset_ostream; - Reset the output stream.

To run the program simply type this into your command prompt:

python NotBF.py /path/to/notbffile.txt

"""
NotBF v0.1.0

---------------------------------------

NotBF is an interpreted Brainfuck-like
language. NotBF is has many similarites
to regular Brainfuck, except without
all the confusing characters.

---------------------------------------
"""
from sys import exit, argv


class NotBFError(object):
    """
    This is the base NotBF error class from which
    all NotBF errors are derived from.
    """
    def __init__(self, message, name, code):
        self.message = message
        self.name = name
        self.code = code

    def raise_error(self):
        """
        Raise an error if something goes wrong.
        """
        print "{0}::{1} -> {2}".format(self.code, self.name, self.message)
        exit(0)


class Environment(object):
    """
    This class provides the data and functions
    for managing a NotBF environment during runtime.
    """
    def __init__(self, stack_size, output_stream,):
        self.stack_size = stack_size
        self.output_stream = output_stream
        self.stack = [0 for _ in range(self.stack_size)]
        self.stack_position = 0
        self.max_stack_pos = len(self.stack)-1
        self.min_stack_pos = 0
        self.max_cell_value = 255
        self.min_cell_value = 0

    def add_output_stream(self, character):
        """
        Add a character to the output stream.
        """
        self.output_stream += character

    def reset_stack(self):
        """
        Reset the stack to it's default length, 
        self.stack_size, and reset all cells.
        """
        self.stack = [0 for _ in range(self.stack_size)]

    def change_stack_size(self, new_size):
        """
        Change the size of the stack. WARNING,
        this operation resets all cell values.
        """     
        self.stack = [0 for _ in range(new_size)]

    def reset_stack_position(self):
        """
        Reset the stack_position to zero.
        """
        self.stack_position = 0

    def change_stack_position(self, new_position):
        """
        Move the stack_position to a new position.
        """
        self.stack_position = new_position

    def change_cell_value(self, new_value):
        """
        Change the value of a cell.
        """
        self.stack[self.stack_position] = new_value

    def output_output_stream(self):
        """
        Output the output stream.
        """
        print self.output_stream

    def reset_output_stream(self):
        """
        Reset the output stream.
        """
        self.output_stream = ""


class NotBFCommand(object):
    """
    A base command class where tokenized input
    is inputted into and then run. All NotBF
    command classes are derived from this base
    class.
    """
    def __init__(self, tokenized_string):
        self.tokenized_string = tokenized_string

    def debug_input(self):
        print self.tokenized_string


"""
Initalize various variables and other
items to make sure that command classes
work the way they should.
"""
runtime_env = Environment(256, "")
integer_error = NotBFError("Invalid integer.", "int_error", "e01")
no_cell_error = NotBFError("Cell doesn't exist.", "no_cell_error", "e02")
bad_value_error = NotBFError("Invalid ASCII code.", "bad_value_error", "e03")
command_error = NotBFError("Invalid command.", "cmd_error", "e04")


class AddOutputStream(NotBFCommand):
    def execute(self):
        character = chr(runtime_env.stack[runtime_env.stack_position])
        runtime_env.add_output_stream(character)


class ResetStack(NotBFCommand):
    def execute(self):
        runtime_env.reset_stack()


class ChangeStackSize(NotBFCommand):
    def execute(self):
        try:
            new_size = int(self.tokenized_string[1])
            runtime_env.change_stack_size(new_size)
        except ValueError:
            integer_error.raise_error()


class ResetStackPosition(NotBFCommand):
    def execute(self):
        runtime_env.reset_stack_position()


class ChangeStackPosition(NotBFCommand):
    def execute(self):
        try:
            new_position = int(self.tokenized_string[1])
            try:
                runtime_env.change_stack_position(new_position)
            except IndexError:
                no_cell_error.raise_error()
        except ValueError:
            integer_error.raise_error()


class ChangeCellValue(NotBFCommand):
    def execute(self):
        try:
            new_value = int(self.tokenized_string[1])
            if new_value <= runtime_env.max_cell_value and new_value >= runtime_env.min_cell_value:
                runtime_env.change_cell_value(new_value)
            else: 
                bad_value_error.raise_error()
        except ValueError:
            integer_error.raise_error()


class OutputOutputStream(NotBFCommand):
    def execute(self):
        runtime_env.output_output_stream()


class ResetOutputStream(NotBFCommand):
    def execute(self):
        runtime_env.reset_output_stream()


class GetCodeInput(object):
    """
    Get code file input from a path.
    """
    def __init__(self, code_file_path):
        self.code_file_path = code_file_path

    def return_file(self):
        """"""
        with open(self.code_file_path, "r") as code_file:
            return code_file.read().replace("\n", "").replace(" ", "").replace("\t", "")


class Tokenizer(object):
    """
    Tokenize given input into a format that is readable
    by the interpreter class. Here's the format.
    tokenized_string = [ [ keyword, arg, ... ], [ ... ], ... ]
    """
    def __init__(self, input_string, line_split=";", arg_split="&"):
        self.input_string = input_string
        self.line_split = line_split
        self.arg_split = arg_split

    def tokenize(self):
        """
        Tokenize the string input into the
        correct format.
        """
        tokenized_string = self.input_string.split(self.line_split)
        tokenized_string = [string.split(self.arg_split) for string in tokenized_string]
        tokenized_string.remove([""])
        print tokenized_string
        return tokenized_string


class Interpreter(object):
    def __init__(self, tokenized_input):
        self.tokenized_input = tokenized_input
        self.COMMAND_KEYS = {
            "add_ostream": AddOutputStream,
            "chg_pos": ChangeStackPosition,
            "reset_stack": ResetStack,
            "chg_size": ChangeStackSize,
            "reset_pos": ResetStackPosition,
            "chg_cell": ChangeCellValue,
            "out_stream": OutputOutputStream,
            "reset_ostream": ResetOutputStream,
        }

    def execute_input(self):
        for line in self.tokenized_input:
            token = line[0]
            if token in self.COMMAND_KEYS:
                command_to_execute = self.COMMAND_KEYS[token](line)
                command_to_execute.execute()
            else: 
                command_error.raise_error()


if __name__ == "__main__":
    code_input = GetCodeInput(argv[1]).return_file()
    tokenized_code = Tokenizer(code_input).tokenize()
    interpreter = Interpreter(tokenized_code).execute_input()

Here's some example code and its output:

chg_cell & 48;
add_ostream;
chg_pos & 1;

chg_cell & 49;
add_ostream;
chg_pos & 1;

reset_stack;
chg_size & 512;

chg_cell & 65;
add_ostream;
chg_pos & 1;

chg_cell & 66;
add_ostream;
chg_pos & 1;

out_stream;
reset_stack;

reset_ostream;

Here's the output of this program:

[['chg_cell', '48'], ['add_ostream'], ['chg_pos', '1'], ['chg_cell', '49'], ['add_ostream'], ['chg_pos', '1'], ['reset_stack'], ['chg_size', '512'], ['chg_cell', '65'], ['add_ostream'], ['chg_pos', '1'], ['chg_cell', '66'], ['add_ostream'], ['chg_pos', '1'], ['out_stream'], ['reset_stack'],['reset_ostream']] 01AB

\$\endgroup\$
  • 2
    \$\begingroup\$ You should add some sample code + output too. \$\endgroup\$ – bhathiya-perera Apr 12 '15 at 8:18
  • 1
    \$\begingroup\$ @JaDogg I've added some example code and output now. \$\endgroup\$ – Ethan Bierlein Apr 12 '15 at 13:50
10
\$\begingroup\$

As the classic "Stop Writing Classes" puts it:

the signature of "this shouldn't be a class" is that it has two methods, one of which is __init__

Virtually all of your classes fall foul of this; just because you can use OOP, doesn't mean you always should. Looking at the use of the classes in the code, this was a big red flag:

code_input = GetCodeInput(argv[1]).return_file()
tokenized_code = Tokenizer(code_input).tokenize()
interpreter = Interpreter(tokenized_code).execute_input()

You're creating the instance and immediately calling a method on it; you don't actually need the instance (or keep it around), you've just split the logic (pretty much arbitrarily) over two methods. The last line assigns interpreter = None, which doesn't even make any sense! Much neater would be simple functions:

code_input = get_code_input(argv[1])
tokenized_code = tokenize(code_input)
interpret(tokenized_code)

Only the Environment is using state in a meaningful way. All of your NotBFCommand subclasses rely on the existence of some global runtime_env; every single one has a single method that just aliases a method on the Environment, so why not just use those methods?

A neater implementation would spot that basically all of your code is about performing operations on the stack. You can therefore have a single class, which encapsulates the Environment (the state; stack and output stream), the NotBFCommands (methods) and NotBFErrors (a method plus some data). For example:

class Interpreter(object):
    """Interpreter for a BrainFuck-ish language."""

    # Cell sizes for the stack
    CELL_MAX = 255
    CELL_MIN = 0

    # Parsing rules
    LINE_SPLIT = ";"
    ARG_SPLIT = "&"

    # Errors
    ERRORS = {
        "integer_error": ("Invalid integer.", "e01"),
        "no_cell_error": ("Cell doesn't exist.", "e02"),
        "bad_value_error": ("Invalid ASCII code.", "e03"),
        "command_error": ("Invalid command.", "e04"),
    }

    def __init__(self, stack_size=256):
        self.stack_size = stack_size
        self.reset_output()
        self.reset_position()
        self.reset_stack()

    def add_output_stream(self):
        """Add the current stack cell to the output."""
        self.output_stream += chr(self.stack[self.pos])

    def change_cell(self, new_val):
        """Change the value of the current cell."""
        new_val = self.convert_int(new_val)
        if self.CELL_MIN <= new_val <= self.CELL_MAX:
            self.stack[self.pos] = new_val
        else:
            self.raise_error("bad_value_error")

    def change_position(self, new_pos):
        """Change stack pointer position, if new position is valid."""
        new_pos = self.convert_int(new_pos)
        if new_pos not in range(len(self.stack)):
            self.raise_error("no_cell_error")
        self.pos = new_pos

    @classmethod
    def convert_int(cls, val):
        """Convert the value to integer or raise an error."""
        try:
            return int(val)
        except ValueError:
            cls.raise_error("integer_error")

    def execute_command(self, command, *data):
        """Execute the command or raise an error."""
        if command not in self.COMMANDS:
            self.raise_error("command_error")
        self.COMMANDS[command](self, *data)

    def print_output(self):
        """Print the current output stream."""
        print self.output_stream

    @classmethod
    def raise_error(cls, name):
        """Report the error and exit."""
        msg, code = cls.ERRORS[name]
        print "{code}::{name} -> {msg}".format(code=code, name=name, msg=msg)
        exit(0)

    @staticmethod
    def remove_whitespace(string):
        """Remove unwanted whitespace from the string."""
        for whitespace in '\t\n ':
            string = string.replace(whitespace, '')
        return string

    def reset_output(self):
        """Reset the output stream."""
        self.output_stream = ""

    def reset_position(self):
        """Reset the stack pointer position to zero."""
        self.pos = 0

    def reset_stack(self):
        """Reset the stack to empty."""
        self.stack = [self.CELL_MIN for _ in range(self.stack_size)]

    def resize_stack(self, new_size):
        """Resize the stack."""
        self.stack_size = self.convert_int(new_size)
        self.reset_stack()

    def run_commands(self, commands):
        """Execute a series of commands."""
        for command in commands:
            self.execute_command(*command)

    def run_file(self, filename, verbose=False):
        """Execute commands from a code file."""
        with open(self.filename, "r") as code_file:
            data = self.remove_whitespace(code_file.read())
        self.run_commands(self.tokenize(data, verbose))

    @classmethod
    def tokenize(cls, command_string, verbose=False):
        """Split the command string into tokens."""
        tokens = []
        for line in command_string.split(cls.LINE_SPLIT):
            if line:
                tokens.append(line.split(cls.ARG_SPLIT))
        if verbose:
            print tokens
        return tokens     

    COMMANDS = {
        "add_ostream": add_output_stream,
        "chg_pos": change_position,
        "reset_stack": reset_stack,
        "chg_size": resize_stack,
        "reset_pos": reset_position,
        "chg_cell": change_cell,
        "out_stream": print_output,
        "reset_ostream": reset_output,
    }

Note that I've added docstrings and followed the style guide. Running the program is now:

if __name__ == "__main__":
    interpreter = Interpreter()
    interpreter.run_file(argv[1], True)

I have also made the stack resizing permanent - if that's not appropriate, you may need to refactor slightly.

\$\endgroup\$
  • \$\begingroup\$ Part of me wants to still make an Environment class and just create an instance of it in Interpreter. Would that be okay? \$\endgroup\$ – Ethan Bierlein Apr 12 '15 at 19:14
  • \$\begingroup\$ @EthanBierlein well it's up to you! What state would an interpreter hold, separate to its environment? How would their interfaces differ? Having the environment be an attribute of the interpreter would certainly be better than your current structure; global state is usually a bad idea. \$\endgroup\$ – jonrsharpe Apr 12 '15 at 19:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.