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!
\$\endgroup\$
2
\$\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.
\$\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.