6
\$\begingroup\$

I have this Brainfuck interpreter:

// Credit goes to where it is due
function throwException(cause, index) {
    // Execute when there is an error in Brainf*** code
    $('p#output').text("Interpreting Terminated because " + cause + " at char index " + index);
}

function runBrainfuck(data) {
    console.time("Running Brainf*** code");
    var memory = new Array(); // memory

    // Initialize memory
    console.time("Initialize memory");
    for(var i = 0; i < 30000; i++) {
        memory[i] = 0;
    }
    console.timeEnd("Initialize memory");

    var p = 0; // pointer to memory
    $('p#output').text("");

    // Loop through the Brainf*** code
    var data = $('#code').val();

    for(var j = 0; j < data.length; j++) {
        switch(true) {
            case data[j] === ">": // go right one cell
               if(p < 30000) {
                   p++;
               } else {
                   throwException("There are only 30000 usable cells of memory.", j); // are you using too many cells?
               }
               break;
            case data[j] === "<": // go left one cell
               if(p > 0) {
                   p--;
               } else {
                   throwException("Cannot decrement pointer when it is at first cell.", j); // are you going below cell 0?
               }
               break;
            case data[j] === "+": // Increment cell value
               memory[p]++;
               if(memory[p] > 255) {
                   memory[p] = 0; // Whoops, overflown the memory!
               }
               break;
            case data[j] === "-": // decrement cell value
               if(memory[p] > 0) {
                   memory[p]--;
               } else {
                   memory[p] = 255; // Overflow back to 255
               }
               break;
            case data[j] === ".": // put character to screen
               var memoryChar = String.fromCharCode(memory[p]);
               if(memoryChar == '\n') {
                   memoryChar = ""; // Turn newlines into linebreaks
               }
               $('p#output').append(String.fromCharCode(memory[p]));
               break;
            case data[j] === ",":
               memory[p] = window.prompt("Please enter one character").charCodeAt(0); // Set memory to the character code
               break;
            case data[j] === "[": // start loop
               if(memory[p] != 0) {
                   continue;
               } else {
                   var openLoopCnt = 0; // number of open loops
                   for(var k = j; k < data.length; k++) {
                       if(data[k] === '[') {
                           openLoopCnt++; // Another open loop is in existence
                       } else if(data[k] === ']') {
                           openLoopCnt--; // Decrement open count

                           if(openLoopCnt === 0) {
                               j = k;
                               break;
                           }
                       }
                   }
                   if(openLoopCnt != 0) {
                       throwException("Open loop.", j);
                   }
               }
               break;
           case data[j] === "]": // end loop
               if(memory[p] != 0) {
                   var closedLoopCnt = 0; // Due to the fact that we are at closing loops, we use closedLoopCnt
                   for(var l = j; l >= 0; l--) {
                        if(data[l] === "]") {
                            closedLoopCnt++;
                        } else if(data[l] === "[") {
                            closedLoopCnt--;
                        }

                        if(closedLoopCnt === 0) {
                            j = l;
                            break;
                        }
                   }
                   if(closedLoopCnt != 0) {
                        throwException("Too many loop closings.", i);
                   }
               }
               break;
        }
    }
    console.timeEnd("Running Brainf*** code");

    console.time("Saving output in localStorage");
    localStorage.setItem("brainfuck_output", $('#output').text());
    console.timeEnd("Saving output in localStorage");
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<h1>Interpret your Brainf*** code!</h1>
<form action="#" method="get" id="brainf***" onsubmit="runBrainfuck($('#code').val())">
    <label for="brainf***">Brainf*** code:</label> <br />
    <textarea name="code" rows="25" cols="70" id="code"></textarea> <br />
    <input type="submit" value="Interpret Brainf*** code!" />
</form>
<h2>Brainf*** output:</h2>
<p id="output"></p>

I will accept any review about cleanliness and other tips for this. I also am wondering if there is a [great] way to rewrite the JavaScript part using jQuery's window.jQuery.submit() function.

\$\endgroup\$
2
  • \$\begingroup\$ For the BF interpreter itself, here's a very much related question and here's its follow-up \$\endgroup\$
    – Flambino
    Commented Mar 15, 2016 at 17:57
  • \$\begingroup\$ But, I am also wondering if there is a way to rewrite the JavaScript using the .submit() method. If no one is going to answer, then I am starting a bounty. It will be risky, but I will if no one answers. \$\endgroup\$ Commented Mar 20, 2016 at 15:22

3 Answers 3

5
+50
\$\begingroup\$

Nice code! In this review, I'll focus on simplification, refactoring, emphasizing OOP, and overall making your code easier to unit test.


You must you proper exception throwing; (you have) no exceptions!

This is an improper way to throw errors:

throwException("There are only 30000 usable cells of memory.", j); // are you using too many cells?

This makes no actual indication to the JavaScript environment that there was any error; it only notifies the user. However, you want to have this function throw errors so you can more easily debug and unit test it.

However, you can just have it start throwing exceptions; there's nothing to catch them. So, instead, you'll have to have some other JavaScript code call this function when the event is fired. Here's what that'd look like:

document.getElementById("brainf***").onsubmit = function() {
    try {
        runBrainfuck(this.code.value);
    } catch {
        // ... show user error...
    }
}

Now, your runBrainfuck function can simply throw errors/exceptions (custom or not) and this will catch them and display them to the user however you like.


switching true off, and finding a better light switch

This is basically what your switch statement is:

switch(true) {
    case a === b:
    case a === c:
    case a === d:
    ...
}

That seems rather abusive of a switch statement. Why not write it as it should be written?

switch(a) {
    case b:
    case c:
    case d:
    ...
}

And, on the note about switches, here's an alternate solution: create a map mapping each character to it's functionality. Here's what I mean:

var commandMap = {
    ">": function() {...
    "<": function() {...
    ...
}

Then, rather than having a long switch statement, you can now use this \$O(1)\$ look-up table:

if(commandMap.hasOwnProperty(data[j])) {
    commandMap[data[j]]();
}

Gettin' some objects OOP in this place

Looking at the scope of your runBrainfuck, there are a lot of "global" variables. Either way, it'd still be nice to refactor some code to keep things clean.

Here's something you could do:

function BrainfuckMachine(tapeSize) {
    this.tape = Array.apply(null, Array(tapeSize)).map(function(a){ return a; });
    this.p = 0;
}

With this class, you can now keep the data separate from your main function. In fact, you could even move that function to be a method of this new class:

 BrainfuckMachine.prototype.runCode = function(code) {

Now your code is more OO and cleaner. Note that with this new class, you now need to pass instances of it to the functions defined in the commandMap so they can access and modify the data. This also allows for easier unit testing.


Setting the tape

A slightly cleaner, more JavaScript-y way to initialize an Array with N copies of a value would be to do this:

Array.apply(null, Array(n)).map(function(a){ return a; });

This would make creating the tape a bit cleaner.


Misc.

  • You are redefining data in your main function. Don't - it's good as a parameter (this allows for unit-testing).
  • Those console.time calls are really distracting.
  • 30000 is a magic number. Don't use magic numbers. Define a constant at the top of your code.
\$\endgroup\$
10
  • \$\begingroup\$ I have a question regarding your answer. Why is 30000 a magic number that I am not supposed to use (becausze you said 30000 was a magic number and you shouldn't use magic numbers)? Also, the console.time() calls are just to see how long it takes to initalize the memory, run the code, and save the output in localStorage. I think instead of having to redundantly use try {} catch() {} in each command, I can use the throw new Error() method. What do you think? \$\endgroup\$ Commented Mar 22, 2016 at 21:43
  • \$\begingroup\$ Here's a link explaining magic numbers and why they're bad: stackoverflow.com/questions/47882/… . I know what the console.time calls are for, but they are still messy and a little distracting. You don't have to redundantly put a try/catch for each command. In the event listener function where you have the original try/catch is where you put that commandMap[data[j]](); snippet. \$\endgroup\$
    – SirPython
    Commented Mar 22, 2016 at 22:23
  • \$\begingroup\$ @Obinna What if you needed to change the 30000 value sometime in the future? You'd have to go through your entire program and manually make the changes. It also isn't clear why you're using a hard limit of 30000. If you assigned it to a constant and gave it a good name, it's self-documenting what it's intended purpose is. \$\endgroup\$ Commented Mar 23, 2016 at 16:53
  • \$\begingroup\$ Actually, @Carcigenicate, I would never intend to do so because a pointer in Brainf*** can freely move around an array of 30000 bytes (from: The Brainfuck Programming Language. Since there is a 30000-byte array of memory in Brainf***, I wouldn't change the 30000 value. \$\endgroup\$ Commented Mar 23, 2016 at 23:50
  • \$\begingroup\$ @ObinnaNwakwue Either way it is good practice. People unfamiliar with that "standard" will be enlightened when they see const/var TAPE_SIZE = 30000 \$\endgroup\$
    – SirPython
    Commented Mar 24, 2016 at 0:23
4
\$\begingroup\$

I'd like to point out two possible enhancements:

Separation from the run-time environment

Currently you code is tightly connected to running inside a browser using jQuery. Since BF has nothing to do with browsers it would make sense to implement it in a way, that it can run in a different environment, say for example Node.

This could be done by providing a object that implements the interaction with the environment. e.g.:

var jQueryInterface = {
  input: function() {
    return window.prompt("Please enter one character").charCodeAt(0);
  },
  output: function(charCode) {
    $('p#output').append(String.fromCharCode(charCode));
  }
};

runBrainfuck(data, jQueryInterface);

function runBrainfuck(data, bfIinterface) {
  // ...
  case data[j] === ".": // put character to screen
               var memoryChar = String.fromCharCode(memory[p]);
               if(memoryChar == '\n') {
                   memoryChar = ""; // Turn newlines into linebreaks
               }
               bfIinterface.output(memory[p]);
               break;
  case data[j] === ",":
               memory[p] = bfIinterface.input();
               break;
  // ...
}

Utilize JavaScript's sparse arrays

In JavaScript you don't need to initialize an array to a fixed length and fill it. Instead when reading from the memory array, when encountering an undefined value, just return 0. Replace all places where your read from the memory with memory[p] with a call to a function such as:

function getMemory(p) {
  return memory[p] || 0; // Both "undefined" and "0" are considered "false". In that case "0" is returned.
}
\$\endgroup\$
4
  • \$\begingroup\$ This is a very smart answer! Your first tip helps keep the code organized and testable, and your second tip just drops the initialization time. +1, definitely. \$\endgroup\$
    – SirPython
    Commented Mar 23, 2016 at 23:36
  • \$\begingroup\$ Wow, I never knew this would work for the implementation of the . command! Thanks! \$\endgroup\$ Commented Mar 23, 2016 at 23:52
  • \$\begingroup\$ By the way, interface is already a reserved keyword, so how about outputInterface? \$\endgroup\$ Commented Mar 24, 2016 at 0:07
  • \$\begingroup\$ I've renamed the interface. \$\endgroup\$
    – RoToRa
    Commented Mar 24, 2016 at 7:29
2
\$\begingroup\$

Your support for looping is completely broken and inefficient — in a language where the only tool for controlling program flow is looping!

This line is the culprit:

openLoopCnt++; // Decrement open count

After fixing that silly bug, you should look into rewriting the interpreter to build a jump table once at the start of execution (like this), so that you don't have to re-scan the code to find the matching delimiter every time you encounter a bracket.

\$\endgroup\$
3
  • \$\begingroup\$ Indeed something is not working, running my ultimate BF interpreter test I get: Interpreting Terminated because There are only 30000 usable cells of memory. at char index 435 \$\endgroup\$ Commented Mar 22, 2016 at 8:02
  • \$\begingroup\$ @SimonForsberg, how many cells did you try to use? You had a maximum of 30000. \$\endgroup\$ Commented Apr 19, 2016 at 0:45
  • \$\begingroup\$ @ObinnaNwakwue I only use about 40 of them. So something is wrong with your interpreter. \$\endgroup\$ Commented Apr 19, 2016 at 10:24

Your Answer

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

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