8
\$\begingroup\$

I've been writing a brainfuck interpreter in python 3. It's nothing serious, but I'm trying to make it as good as possible (considering program structure, user experience and etc) for my own learning purpose. Any suggestion is welcomed.

"""A brainfuck interpreter written in Python 3.5."""

import argparse

__all__ = ['Brainfuck']

class Brainfuck:
    """Brainfuck interpreter main class."""

    def __init__(self, src, debug=False):
        """
        Parameters
        ----------
        src: string
            Brainfuck code to be evaluated.
        debug: bool (optional, default: False)
            Pause at '#' and print some status information.

        Public attributes
        -----------------
        output: string
            Output of the brainfuck script.
        """
        self._cells = [0]
        self._cell_ptr = 0

        self._src = self._clean(src, debug)
        self._src_ptr = 0

        self._open_bracket_indexes = []
        self._close_bracket_indexes = []
        self._pair_brackets()

        self.output = ''

        self._evaluate()

    @property
    def _cell_value(self):
        return self._cells[self._cell_ptr]

    @_cell_value.setter
    def _cell_value(self, value):
        self._cells[self._cell_ptr] += value

        if self._cells[self._cell_ptr] > 255:
            self._cells[self._cell_ptr] = 0

        elif self._cells[self._cell_ptr] < 0:
            self._cells[self._cell_ptr] = 255

    @property
    def _command(self):
        return self._src[self._src_ptr]

    @staticmethod
    def _clean(src, debug):
        commands = '+-<>[],.#' if debug else '+-<>[],.'
        return ''.join(c for c in src if c in commands)

    def _pair_brackets(self):
        """
        _open_bracket_indexes[i] is paired with _close_bracket_indexes[i].
        """
        stack = []

        for index, command in enumerate(self._src):
            if command == '[':
                stack.append(index)

            elif command == ']':
                self._open_bracket_indexes.append(stack.pop())
                self._close_bracket_indexes.append(index)

    def _evaluate(self):
        while self._src_ptr <= len(self._src)-1:
            command = self._command

            if command == '+':
                self._cell_value = 1

            elif command == '-':
                self._cell_value = -1

            elif command == '<':
                self._cell_ptr -= 1

            elif command == '>':
                self._cell_ptr += 1

                if self._cell_ptr > len(self._cells)-1:
                    self._cells.append(0)

            elif command == '[' and self._cell_value == 0:
                self._to_paired_close_bracket()

            elif command == ']' and self._cell_value != 0:
                self._to_paired_open_bracket()

            elif command == ',':
                self._cell_value = ord(input('> ')[0])

            elif command == '.':
                new_char = chr(self._cell_value)

                self.output += new_char
                print(new_char, end='')

            elif command == '#':
                self._print_status()

            self._src_ptr += 1

    def _to_paired_open_bracket(self):
        index = self._close_bracket_indexes.index(self._src_ptr)
        self._src_ptr = self._open_bracket_indexes[index]

    def _to_paired_close_bracket(self):
        index = self._open_bracket_indexes.index(self._src_ptr)
        self._src_ptr = self._close_bracket_indexes[index]

    def _print_status(self):
        cells_string = self._cells_string()
        cell_ptr_string = self._cell_ptr_string(cells_string)

        print('\n---------')
        print('CELLS:    {}'.format(cells_string))
        print('POINTER:  {}'.format(cell_ptr_string))
        print('POSITION: {}'.format(self._cell_ptr))
        print('---------')

        input('[DEBUGGING] \'enter\' to continue')

    def _cells_string(self):
        """
        Transform cells array into a string.

        Example:
            array: [0, 87, 100, 33, 10]
            string: |0| 87| 100| 33| 10|
        """
        return (str(self._cells).replace('[', '|')
                                .replace(',', '|')
                                .replace(']', '|'))

    def _cell_ptr_string(self, cells_string):
        """
        Example:
            _cell_ptr: 3
            cells_string:    |0| 87| 100| 33| 10|
            cell_ptr_string:               ^
        """
        index = self._cell_ptr_index_on_cells_string(cells_string)
        return ' ' * (index-1) + '^'

    def _cell_ptr_index_on_cells_string(self, cells_string):
        """Calculate the index of '^' in cell_ptr_string."""
        n = self._cell_ptr + 2
        index = cells_string.find('|')

        while n > 1 and index >= 0:
            index = cells_string.find('|', index+1)
            n -= 1

        return index

def _parser():
    p = argparse.ArgumentParser()

    p.add_argument('file', help='brainfuck file to be evaluated')
    p.add_argument('-d', '--debug',
                   action='store_true',
                   help='pause at \'#\' and print some status information')

    return p.parse_args()

def _main():
    args = _parser()

    with open(args.file) as file:
        src = file.read()
        Brainfuck(src, args.debug)

if __name__ == '__main__':
    _main()
\$\endgroup\$
8
  • \$\begingroup\$ Does this code actually work? it looks like command = self._command is missing something. Is it supposed to be command = self._command[self._src_ptr]? \$\endgroup\$
    – skrrgwasme
    Jul 11, 2016 at 20:13
  • \$\begingroup\$ It works. self._command is a property, which returns self._src[self._src_ptr]. \$\endgroup\$
    – Oliver
    Jul 11, 2016 at 20:51
  • \$\begingroup\$ Looking at it now, maybe it doesn't need to be a property, because I only call it once.. Right? \$\endgroup\$
    – Oliver
    Jul 11, 2016 at 20:56
  • \$\begingroup\$ I think making it a property is unnecessary. The point of properties is to hide complexity from code that uses your classes, and allow them to treat some complex value retrieval/calculation as a simple attribute. Since it's an internal object member, there's nothing to hide, so it introduces complexity without much benefit.. My opinion would be that it's unnecessarily complex, and should be a simple list index. \$\endgroup\$
    – skrrgwasme
    Jul 11, 2016 at 21:02
  • \$\begingroup\$ Thanks for the explanation. I'm new to this site, should I edit the code above right away? \$\endgroup\$
    – Oliver
    Jul 11, 2016 at 21:13

2 Answers 2

2
\$\begingroup\$

Your implementation of the , command, using input('> '), is non-standard. Standard Brainfuck shouldn't print a prompt, nor should it wait for Return to be pressed. Rather, it should just read one byte from sys.stdin.

The condition

while self._src_ptr <= len(self._src)-1:

… would be more idiomatically written as

while self._src_ptr < len(self._src):

Similarly simplify if self._cell_ptr > len(self._cells)-1.

What would you guess that

self._cell_value = 1

does? I find it deceptive that it increments the cell value by 1 (with wraparound) rather than setting the value. I recommend writing it as self._cell_value += 1. Keep the wraparound logic in the setter, if you want.

\$\endgroup\$
1
\$\begingroup\$

Looks like fairly reasonable code to me.

In _clean(), rather than commands = '+-<>[],.#' if debug else '+-<>[],.', consider setting the value and then conditionally appending '#' if we're debugging. It's just easier to read, to avoid needing to visually diff them.

In _evaluate(), the giant if is perfectly nice, but there is an opportunity to dispatch using a dict that maps from character to code, if you like. And this expression:

    while self._src_ptr <= len(self._src)-1:

is less natural than a for loop over the "good part" of _src, perhaps using a slice.

EDIT: The expression is odd enough that I just mentally flagged it as "odd" and moved on, without being curious enough to consider its details. Thank you, 200_success, for pointing out that <= is just trouble there. I saw the - 1 and thought that for command in self._src[:-1]: was what was intended, but no, it's actually saying for command in self._src:, just not in the obvious way. There should be one - and preferably only one -obvious way to do it.

\$\endgroup\$
1
  • \$\begingroup\$ Could you be more specific about what you mean by 'a for loop over the "good part" of _src'? \$\endgroup\$ Jan 17, 2018 at 3:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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