1
\$\begingroup\$

Recently wrote a brainfuck interpreter in Go.

Here's the Github Repo Brainfuck

Areas that need work

The parsing for brackets [] is still buggy and is known to fail when dealing with nested loops i.e [[]] any hints on how to approach the problem would be greatly appreciated.

After looking at this repo. He creates a compiler and then a parser to call the opcodes. Is this type of design common in writing interpreters? And should I use this design approach in my interpreter?

The test suite is very basic and needs more test cases, for instance, I'm using scanner.Scan() to get input from the user but how do I write a test case for that? Further, there is a -file that grabs the contents of a file when given a path to the file but I don't have a good idea of how to test it.

The overall structure of my code reeks of bad code smell, how can I improve it?

brainfuck.go

package main
import (
 "bufio"
 "flag"
 "fmt"
 "io/ioutil"
 "os"
 "strings"
)
const memorySize = 30000
const cellLimit = 256
// incrementPointer
func incrementPointer(mem []int, ptr *int) error {
 if *ptr >= len(mem)-1 {
 return fmt.Errorf("memory error: %d", ptr)
 }
 *ptr++
 return nil
}
// decrementPointer
func decrementPointer(mem []int, ptr *int) error {
 if *ptr < 0 {
 return fmt.Errorf("memory error: %d", ptr)
 }
 *ptr--
 return nil
}
// incrementByte
func incrementByte(mem []int, ptr *int) {
 if mem[*ptr] == cellLimit {
 mem[*ptr] = 0
 }
 mem[*ptr]++
}
// decrementByte
func decrementByte(mem []int, ptr *int) {
 if mem[*ptr] == 0 {
 mem[*ptr] = cellLimit
 }
 mem[*ptr]--
}
// outputByte
func outputByte(mem []int, ptr *int) string {
 return fmt.Sprintf("%c", mem[*ptr])
}
// storeByte
func storeByte(mem []int, ptr *int) {
 var s string
 fmt.Print("Waiting for input: ")
 fmt.Scanln(&s)
 mem[*ptr] = int([]byte(s)[0])
}
func main() {
 var memory = [...]int{memorySize: 0}
 var pointer int
 var result string
 var leftBracket int
 var rightBracket int
 filename := flag.String("file", "", "choose a brainfuck file.")
 flag.Parse()
 file, err := ioutil.ReadFile(*filename)
 if err != nil && *filename != "" {
 fmt.Print(err)
 os.Exit(1)
 }
 tfile := strings.TrimSuffix(string(file), "\n")
 scanner := bufio.NewScanner(os.Stdin)
 if tfile == "" {
 scanner.Scan()
 }
 text := scanner.Text()
 var input string
 if text != "" {
 input = strings.TrimSuffix(text, "\n")
 } else if tfile != "" {
 input = tfile
 } else {
 fmt.Print("nothing to parse.")
 os.Exit(1)
 }
 for i := 0; i < len(input); i++ {
 switch input[i] {
 case '>':
 incrementPointer(memory[:], &pointer)
 case '<':
 decrementPointer(memory[:], &pointer)
 case '+':
 incrementByte(memory[:], &pointer)
 case '-':
 decrementByte(memory[:], &pointer)
 case '.':
 result += outputByte(memory[:], &pointer)
 case ',':
 storeByte(memory[:], &pointer)
 case '[':
 leftBracket = i
 case ']':
 rightBracket = i
 i = leftBracket
 if memory[pointer] == 0 {
 i = rightBracket
 }
 default:
 continue
 }
 }
 fmt.Printf("%s\n", result)
}

brainfuck_test.go

package main
import (
 "testing"
 // "fmt"
)
func TestIncrementPointer(t *testing.T) {
 mem := make([]int, 4)
 ptr := 0
 incrementPointer(mem, &ptr)
 if ptr != 1 {
 t.Errorf("incrementPointer was incorrect, got %d instead", ptr)
 }
}
func TestDecrementPointer(t *testing.T) {
 mem := make([]int, 4)
 ptr := 1
 decrementPointer(mem, &ptr)
 if ptr != 0 {
 t.Errorf("decrementPointer was incorrect, got %d instead", ptr)
 }
}
func TestIncrementByte(t *testing.T) {
 mem := make([]int, 4)
 ptr := 0
 want := 1
 incrementByte(mem, &ptr)
 if mem[ptr] != want {
 t.Errorf("incrementByte was incorrect, got %d instead", ptr)
 }
}
func TestDecrementByte(t *testing.T) {
 mem := make([]int, 4)
 ptr := 0
 want := 255
 decrementByte(mem[:], &ptr)
 
 if mem[ptr] != want {
 t.Errorf("decrementByte was incorrect, got %d instead", ptr)
 }
}

Example

$./brainfuck *ENTER*
>++++++++[<+++++++++>-]<.>++++[<+++++++>-]<+.+++++++..+++.>>++++++[<+++++++>-]<++.------------.>++++++[<+++++++++>-]<+.<.+++.------.--------.>>>++++[<++++++++>-]<+
$ Hello, World!
asked May 20, 2021 at 23:48
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Structure

I would expect to find something that just runs Brainfuck code, like a method or a function, not have this embedded into the main function. Similarly, retrieving the code from stdin or from a file could be moved to a separate function. It would make these things easier to understand in separation. Also, concerning this separation, get rid of the habit of declaring variables (memory, pointer etc) far away from where they are actually used.

I'd expect these functions to also return an error, to comply with Go conventions. That would also imply that you abort execution when you get an overflow/underflow for the pointer or other fatal mistakes. If you decide to put this into a class, its state would be the memory, the pointer and the stack (I guess you'll need one) with brackets.

I'm not sure how far you want to go, but maybe you want to extend tests to the functions with side effects, i.e. input and output. In that case, it would help if you defined an IO interface which can be plugged into the interpreter. It would allow creating a mock for tests.

Notes and Bugs

  • Increment and decrement byte operations are broken, they still increment the byte on overflow, after zeroing it. Consider using an explicit int8 or uint8 (not sure about Brainfuck's spec there). Also, Go should have a modulo function which you could use to avoid these issues.
  • There's some "dead" code:
    • result is used, but not really useful. Separating things probably would have shown that.
    • Commented out import fmt in testfile.
    • Trailing empty line in functions.
    • Comments for functions that only repeat the name of the function.
  • An isolated right bracket is currently ignored. I'm not sure if that is intended.
  • Invalid characters in the input are ignored. I'm not sure if that is intended either.
answered May 21, 2021 at 9:31
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.