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!
1 Answer 1
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
oruint8
(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.