7
\$\begingroup\$

Many Brainfuck implementations I've come across generally are obtuse, verbose or over-the-top. But I've always seen Brainfuck as "easy to implement", especially for beginners.

My original need for this was to be able to have an actual Brainfuck "machine" that executed instructions to aid with code generation. However, this should also work fine on its own.

How can the "API" be improved?

#include <iostream>
#include <stack>

// Mini BF interpreter
struct BF
{
    int tape[30000] = {0};
    int *ptr = tape;
    std::string program = "";

    /* The index will be used to control program flow
     * when encountering brackets.
     */
    std::size_t index = 0;
    std::stack<int> brackets;

    BF(std::string program = "")
        : program(program)
    {
    }

    void addc(char c)
    {
        program += c;
    }

    void adds(std::string s)
    {
        program += s;
    }

    void run()
    {
        while (next());
    }

    void print_memory()
    {
        for (int i = 0; i < 100; ++i)
            std::cout << tape[i] << " ";
        std::cout << "\n";
    }

    bool next()
    {
        if (index < program.size())
        {
            token(program[index++]);
            return true;
        }
        return false;
    }

    void token(char c)
    {
        switch (c)
        {
            case '+': ++*ptr; break;
            case '-': --*ptr; break;
            case '>': ++ptr; break;
            case '<': --ptr; break;
            case '[':
                if (*ptr)
                {
                    // Save position of bracket
                    brackets.push(index);
                } else {
                    // Skip to the matching bracket
                    index = program.find(']');
                }
                break;
            case ']':
                if (*ptr)
                {
                    // Jump to the last bracket
                    index = brackets.top();
                } else {
                    // Loop is finished
                    brackets.pop();
                }
                break;
            case '.':
                std::cout << (char)*ptr;
                break;
            default:
                break;
        }
    }
};

int main()
{
    std::string program{"++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."};
    BF bf{program};
    bf.run();
    bf.print_memory();
}
\$\endgroup\$
  • 1
    \$\begingroup\$ The whole point about Brainfuck being created in the first place was that it should need an as small interpreter as possible, so you are correct that Brainfuck is supposed to be "easy to implement". I guess some of us around here (erhm, me...) just like to take things to the next level ;) \$\endgroup\$ – Simon Forsberg Sep 6 '15 at 13:58
2
\$\begingroup\$

Your program is small and well written, so there's not much else to change, IMO. My comments are mostly on code aesthetics.

  • Your tape is a fixed size array, but you are not checking if you ran out of tape/memory. A very big program could crash your interpreter. This is a simple fix that might be worth it, it shouldn't add too much complexity to the code. One simple way is to replace every raw increment/decrement of ptr by a function that checks if you've ran out of space. Then "safely" terminate the interpreter with a friendly error message, instead of a segfault.

  • Also, an array of 30000 ints is a lot of memory. Declaring an instance of the interpreter as a local function-level variable might stress the program stack. Maybe replace it by a dynamically allocated container like a std::vector.

  • No need to explicitly initialize the string std::string program = "". A std::string is a class, so it has a constructor that default initializes it to empty.

  • In the constructor, use std::move to move the input string, instead of copying it. When you pass by value, C++ always copies the data. You'll want to avoid that for a long string such as the program text.

    BF(std::string program = "")
        : program(std::move(program))
    {
    }
    
  • addc and adds are not used. Nevertheless, spell out their names: add_char and add_string.

  • print_memory only prints the first 100 values of tape. You should comment the reason for that, otherwise, readers will assume it was a mistake, since tape is a lot larger. An alternative it to take the number of entries to print as a parameter of the function. Also relevant, C++ has the concept of const member functions, which are functions guaranteed not to change any member data. That's the case with print_memory, so it should be const:

    void print_memory() const { /* ... */ }
                        ^^^^^
    
  • Make your interpreter a class and turn the internal methods and data private. In a structure, all fields are accessible to users of the object. It is better to hide implementation details and also to prevent users from messing with the internal interpreter states. Only leave the interface methods like run() in the public section of your class.

  • Very minor thing, but I'd change this: while (next()); to while (next()) { } to make it more visible that that's an empty loop.

  • Another tiny thing: I usually dislike short names or abbreviations, so I'd name the class BFInterpreter instead.


Lastly, there are two other very good implementations here on CR you should take a look:

\$\endgroup\$

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.