4
\$\begingroup\$

I have recently written a Brainfuck interpreter in C#. I tested it with the examples given on EsoLang website. It does not handle errors right now.

Questions:

  1. Even that it can run only one program at once, should I maybe put the variables into the class (create new class) or just leave them inside the BF method?
  2. Should I split all of the code from the cases and create separate methods for them, accessing them from the switch?

Of course, I am also looking for all other suggestions on how I can improve this code.

using System;

namespace BF
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            if (args.Length == 0) {
                Console.WriteLine("Specify the source file!");
            } else {
                if (System.IO.File.Exists(args[0]) == true) {
                    BF(System.IO.File.ReadAllText(args[0]).ToCharArray());
                } else {
                    Console.WriteLine("The path to the file is not valid!");
                }
            }


        }

        private static void BF(char[] instructions)
        {
            int instructionPointer = 0;

            int[] memory = new int[30000];

            int pointer = 1;

            while (instructionPointer < instructions.Length) {

                switch (instructions[instructionPointer]) {

                    case '>': {
                            pointer += 1;
                            break;
                        }

                    case '<': {
                            pointer -= 1;
                            break;
                        }

                    case '+': {
                            memory[pointer] += 1;
                            break;
                        }

                    case '-': {
                            memory[pointer] -= 1;
                            break;
                        }

                    case '.': {
                            Console.Write((char)memory[pointer]);
                            break;
                        }

                    case ',': {
                            memory[pointer] = byte.Parse(Console.Read().ToString());
                            break;
                        }
                    case '[':
                        {
                        if (memory[pointer] == 0) {
                            int s = 0;
                            int ptr = instructionPointer + 1;
                            while (instructions[ptr] != ']' || s > 0) {
                                if (instructions[ptr] == '[') {
                                    s += 1;
                                } else if (instructions[ptr] == ']') {
                                    s -= 1;
                                }
                                ptr += 1;
                                instructionPointer = ptr;
                                }
                            }
                            break;
                        }

                    case ']': {
                            if (memory[pointer] != 0) {
                                int s = 0;
                                int ptr = instructionPointer - 1;
                                while (instructions[ptr] != '[' || s > 0) {
                                    if (instructions[ptr] == ']') {
                                        s += 1;
                                    } else if (instructions[ptr] == '[') {
                                        s -= 1;
                                    }
                                    ptr -= 1;
                                    instructionPointer = ptr;
                                }
                            }
                            break;
                        }
                }


                instructionPointer += 1;

            }




        }
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Hey, welcome to CodeReview! Hope you get some nice reviews. \$\endgroup\$ – Graipher Oct 20 '16 at 21:38
  • \$\begingroup\$ You can stack switch conditions \$\endgroup\$ – paparazzo Oct 20 '16 at 23:04
3
\$\begingroup\$
int pointer = 1;

Looks like you're throwing away memory[0] here; I'd expect the pointer to initialize at the very start of the "tape".


if (System.IO.File.Exists(args[0]) == true)

The == true part is redundant, and the fully-qualified File.Exists feels... crowded. I'd remove System.IO. and add using System.IO; at the top of the file.

Arguably if no argument is specified, you should be throwing some ArgumentException instead of just saying "specify the source file" and exiting with a code-0, which would be interpreted as a successful run. Such a guard clause would help reduce nesting in the Main procedure:

    public static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            throw new ArgumentException("No source file was specified.");
        }
        if (!File.Exists(args[0]))
        {
            throw new FileNotFoundException("Specified source file was not found.");
        }

        BF(System.IO.File.ReadAllText(args[0]).ToCharArray());
    }

Notice the consistency with brace positioning; your code alternates between C#-style and Java-style braces, depending on whether we're looking at a member or its body - it really doesn't matter which one you prefer, but pick a style, and stick to it.


"BF" might perhaps possibly make a [bad] class name - it's a noun. But here BF is a method, and methods do something, they're verbs. Interpret or even Run would be a better choice.


BF is a simple language; you don't need to go out of your way and implement a lexer to tokenize input and a parser to make sense out of it - basically every single character is an instruction, so it makes sense to process it one character at a time.

But is it ideal to read an entire file's contents, iterate every single character to create a character array, and then pass that array to a method that will iterate it one item at a time? Seems rather inefficient, considering you could be streaming the file content into the interpreter, and finish interpreting the BF program as you finish reading the last character in the file, having iterated the contents exactly once.


I think I'd make a separate dedicated class for the interpreter, with the pointer and memory[] as instance fields; I'd make a Dictionary<char, Action> to map each BF token to a dedicated method, too.

\$\endgroup\$
  • \$\begingroup\$ Thanks! I didn't notice this int pointer = 1;. \$\endgroup\$ – 107MP Oct 20 '16 at 22:27
  • 1
    \$\begingroup\$ A counter argument against streaming the code is that BF programs can have loops, so you have to keep most of the code in memory anyway. \$\endgroup\$ – Roland Illig Oct 20 '16 at 22:27
  • \$\begingroup\$ @RolandIllig wouldn't only the loop body need to stick around though? I've never written a BF interpreter, ...your comment makes me want to give it a shot :-) \$\endgroup\$ – Mathieu Guindon Oct 20 '16 at 23:01
  • 1
    \$\begingroup\$ The code from the question is the most straight-forward way to write a BF interpreter. When you want to discard code from memory, you can only do if after proving that it is dead code. This will make the interpreter so much more complicated that it is usually not done. You would need the typical data flow analysis algorithms for this task. \$\endgroup\$ – Roland Illig Oct 20 '16 at 23:04
  • 1
    \$\begingroup\$ @RubberDuck lol, I started making one in C# 6, got the lexer done, hoping to finish the parser and the interpreter tonight. Not the "simple BF interpreter", but the work will be useful for RD's 2.x syntax trees :-) \$\endgroup\$ – Mathieu Guindon Oct 21 '16 at 14:04
2
\$\begingroup\$
namespace BF
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            if (args.Length == 0) {
                Console.WriteLine("Specify the source file!");

(Functionality) Since the above is an error message, it should be written using Console.Error.WriteLine. Additionally, you should call Environment.Exit(1); to tell the calling program about the usage error.

            } else {
                if (System.IO.File.Exists(args[0]) == true) {

(Style) You can omit the == true.

                    BF(System.IO.File.ReadAllText(args[0]).ToCharArray());
                } else {
                    Console.WriteLine("The path to the file is not valid!");
                }
            }


        }

(Style) Why do you have two empty lines between the above closing braces? Usually, one empty line is enough, and in this case, I would prefer to have zero empty lines.

        private static void BF(char[] instructions)
        {
            int instructionPointer = 0;

            int[] memory = new int[30000];

            int pointer = 1;

(Functionality) You should initialize pointer = 0, since the first valid index for a C# array is 0.

            while (instructionPointer < instructions.Length) {

                switch (instructions[instructionPointer]) {

                    case '>': {
                            pointer += 1;

(Style) Instead of pointer += 1, it is more common to write pointer++.

                            break;
                        }

                    case '<': {
                            pointer -= 1;

(Style) This one should be pointer--.

                            break;
                        }

                    case '+': {
                            memory[pointer] += 1;
                            break;
                        }

(Style) As long as you don't declare any local variables, the braces in each of the cases are not necessary. A case statement takes quite a lot of screen space because of all the break statements, so enclosing them in braces wastes even more space.

                    case '-': {
                            memory[pointer] -= 1;
                            break;
                        }

                    case '.': {
                            Console.Write((char)memory[pointer]);

(Functionality) Using three different types for the memory cells makes the Brainfuck programs hard to debug. You use int for the loops, char when writing a character and byte when reading a character. This is very confusing. Choose one of the types and use it in all places.

                            break;
                        }

                    case ',': {
                            memory[pointer] = byte.Parse(Console.Read().ToString());

(Functionality) It is simpler to just say memory[pointer] = (byte) Console.Read(). Or just leave out the type cast and simply say memory[pointer] = Console.Read().

                            break;
                        }
                    case '[':
                        {
                        if (memory[pointer] == 0) {
                            int s = 0;
                            int ptr = instructionPointer + 1;
                            while (instructions[ptr] != ']' || s > 0) {
                                if (instructions[ptr] == '[') {
                                    s += 1;
                                } else if (instructions[ptr] == ']') {
                                    s -= 1;
                                }
                                ptr += 1;
                                instructionPointer = ptr;
                                }

(Style) The closing brace is indented at the same level as the statements above it. This looks wrong.

                            }
                            break;
                        }

                    case ']': {
                            if (memory[pointer] != 0) {
                                int s = 0;

(Style) Why did you call this variable s? I would understand level, depth or some similar name, but s is usually a name for a string.

                                int ptr = instructionPointer - 1;
                                while (instructions[ptr] != '[' || s > 0) {
                                    if (instructions[ptr] == ']') {
                                        s += 1;
                                    } else if (instructions[ptr] == '[') {
                                        s -= 1;
                                    }
                                    ptr -= 1;
                                    instructionPointer = ptr;
                                }

(Functionality) If the brainfuck program has unbalanced loop brackets, your code will throw an exception because ptr will be outside the valid range for the memory index. You have to choose whether you want to add error handling code here.

                            }
                            break;
                        }
                }


                instructionPointer += 1;

            }




        }
    }
}

(Style) The many empty lines look unorganized. Just remove them, unless you want to organize your code into paragraphs for human readers. Then use one empty line for that.

Overall: The code is easy to read and nicely structured. There are only some small stylistic things and some bits around error handling that could be improved.

\$\endgroup\$
  • \$\begingroup\$ Thanks a lot! I'll load the file to an char array as you suggested. \$\endgroup\$ – 107MP Oct 20 '16 at 22:35
  • 1
    \$\begingroup\$ Very nice review here, many nice catches, keep 'em coming! IMO s is just a bad name though ;-) \$\endgroup\$ – Mathieu Guindon Oct 20 '16 at 23:03
  • 1
    \$\begingroup\$ Yeah that wasn't a nice name, I did it by an accident as my native language is Polish. s stands for Stopień here. \$\endgroup\$ – 107MP Oct 21 '16 at 4:32

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.