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!