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.
3 Answers 3
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.
switch
ing 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.
-
\$\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 inlocalStorage
. I think instead of having to redundantly usetry {} catch() {}
in each command, I can use thethrow new Error()
method. What do you think? \$\endgroup\$Obinna Nwakwue– Obinna Nwakwue2016年03月22日 21:43:29 +00:00Commented 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– SirPython2016年03月22日 22:23:22 +00:00Commented 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\$Carcigenicate– Carcigenicate2016年03月23日 16:53:37 +00:00Commented 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\$Obinna Nwakwue– Obinna Nwakwue2016年03月23日 23:50:42 +00:00Commented 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– SirPython2016年03月24日 00:23:58 +00:00Commented Mar 24, 2016 at 0:23
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.
}
-
\$\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– SirPython2016年03月23日 23:36:45 +00:00Commented Mar 23, 2016 at 23:36
-
\$\begingroup\$ Wow, I never knew this would work for the implementation of the
.
command! Thanks! \$\endgroup\$Obinna Nwakwue– Obinna Nwakwue2016年03月23日 23:52:28 +00:00Commented Mar 23, 2016 at 23:52 -
\$\begingroup\$ By the way,
interface
is already a reserved keyword, so how aboutoutputInterface
? \$\endgroup\$Obinna Nwakwue– Obinna Nwakwue2016年03月24日 00:07:47 +00:00Commented Mar 24, 2016 at 0:07 -
\$\begingroup\$ I've renamed the interface. \$\endgroup\$RoToRa– RoToRa2016年03月24日 07:29:24 +00:00Commented Mar 24, 2016 at 7:29
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.
-
\$\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\$Simon Forsberg– Simon Forsberg2016年03月22日 08:02:06 +00:00Commented Mar 22, 2016 at 8:02 -
\$\begingroup\$ @SimonForsberg, how many cells did you try to use? You had a maximum of 30000. \$\endgroup\$Obinna Nwakwue– Obinna Nwakwue2016年04月19日 00:45:46 +00:00Commented Apr 19, 2016 at 0:45
-
\$\begingroup\$ @ObinnaNwakwue I only use about 40 of them. So something is wrong with your interpreter. \$\endgroup\$Simon Forsberg– Simon Forsberg2016年04月19日 10:24:24 +00:00Commented Apr 19, 2016 at 10:24
.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\$