go-brainfuck-yourself

A Brainfuck interpreter that I wrote myself in Go
git clone git@abtrout.com:go-brainfuck-yourself.git
Log | Files | Refs | README

commit acf3448fc0a4e4066a858ae02bc6340ec0ae53d7
parent 64d851fab8598d35c4a98bf549d376d8cafabe91
Author: david cochran <about.trout@gmail.com>
Date:   Sat, 18 Nov 2023 13:38:14 -0800

fixes #3 - support "streaming" command evaluation

Diffstat:
MREADME.md | 29++++++++++++++++++++++++++++-
Abf.go | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abf_test.go | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/gbfy/main.go | 73+++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Dgbfy.go | 141-------------------------------------------------------------------------------
Dgbfy_test.go | 144-------------------------------------------------------------------------------
6 files changed, 321 insertions(+), 312 deletions(-)

diff --git a/README.md b/README.md @@ -1,3 +1,30 @@ # `gbfy` -A [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) interpreter in Golang. +A [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) interpreter that I wrote myself in Go. + + +``` +$ cat test.bf +,>,<[>+<-]>. +$ hexdump -C input.bytes +00000000 01 02 |..| +00000002 +$ ./gbfy -f test.bf -i input.bytes -repl +2023/12/02 18:41:06 Output from execution: [3] +2023/12/02 18:41:06 Starting interactive REPL +Welcome! + :q[uit] to exit REPL loop + :d[ump] to dump interpreter state + :f[uck] to reset interpreter state +gbfy> :d +cmds: ",>,<[>+<-]>." +i: 12, current cmd: '.' +cells: [0 3 0 0 0 0 0 0 0 0] +d: 1, current cell: 3 +gbfy> +++++ +gbfy> :d +cmds: ",>,<[>+<-]>.+++++" +i: 17, current cmd: '+' +cells: [0 8 0 0 0 0 0 0 0 0] +d: 1, current cell: 8 +``` diff --git a/bf.go b/bf.go @@ -0,0 +1,124 @@ +package gbfy + +import ( + "bytes" + "errors" + "fmt" +) + +type Brainfuck struct { + cells [3e4]byte // cells + d int // data pointer for cells access + cmds []byte // commands evaluated by the interpreter + i int // instruction pointer for cmds access + + in, out *bytes.Buffer // input and output buffers + + loops map[int]int // stores index for matching [ or ] + parLoops []int // indices for not-yet-complete loops +} + +// New returns a new Brainfuck interpreter. +func New(in, out *bytes.Buffer) *Brainfuck { + return &Brainfuck{in: in, out: out, loops: map[int]int{}} +} + +// Run runs a Brainfuck program and returns output bytes or error. +func Run(program string, in *bytes.Buffer) ([]byte, error) { + var out bytes.Buffer + bf := New(in, &out) + for _, cmd := range []byte(program) { + if err := bf.Eval(cmd); err != nil { + return nil, err + } + } + return out.Bytes(), nil +} + +// Eval evaluates a single command with the given interpreter. +func (bf *Brainfuck) Eval(cmd byte) error { + switch cmd { + case '>', '<', '+', '-', '.', ',', '[', ']': + bf.cmds = append(bf.cmds, cmd) + + // TODO: Move this special loop handling to internal .eval? + j := len(bf.cmds) - 1 + // Special handling for loops. + if cmd == '[' { + bf.parLoops = append(bf.parLoops, j) + } + if cmd == ']' { + if len(bf.parLoops) == 0 { + return errors.New("invalid loop close") + } + k := bf.parLoops[len(bf.parLoops)-1] + bf.loops[j] = k + bf.loops[k] = j + bf.parLoops = bf.parLoops[:len(bf.parLoops)-1] + } + return bf.eval() + default: + return nil + } +} + +func (bf *Brainfuck) eval() error { + if len(bf.parLoops) > 0 { + // Delay evaluation if there are unclosed loops. + return nil + } + + for bf.i < len(bf.cmds) { + switch bf.cmds[bf.i] { + case '>': + bf.d++ + if bf.d >= len(bf.cells) { + bf.d -= len(bf.cells) + } + case '<': + bf.d-- + if bf.d < 0 { + bf.d += len(bf.cells) + } + case '+': + bf.cells[bf.d]++ + case '-': + bf.cells[bf.d]-- + case '.': + bf.out.WriteByte(bf.cells[bf.d]) + case ',': + if b, err := bf.in.ReadByte(); err != nil { + return fmt.Errorf("failed to readDataVal: %v", err) + } else { + bf.cells[bf.d] = b + } + case '[': + if bf.cells[bf.d] == 0 { + bf.i = bf.loops[bf.i] + } + case ']': + if bf.cells[bf.d] != 0 { + bf.i = bf.loops[bf.i] + } + } + bf.i++ + } + return nil +} + +// Dump interpreter state to caller. +func (bf *Brainfuck) Dump() (int, []byte, int, []byte, []byte) { + return bf.d, bf.cells[:], bf.i, bf.cmds, bf.out.Bytes() +} + +// Reset interpreter state. +func (bf *Brainfuck) Reset() { + bf.d = 0 + bf.cells = [len(bf.cells)]byte{} + + bf.i = 0 + bf.cmds = nil + + bf.loops = map[int]int{} + bf.parLoops = nil +} diff --git a/bf_test.go b/bf_test.go @@ -0,0 +1,122 @@ +package gbfy + +import ( + "bytes" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEval(t *testing.T) { + tests := []struct { + // Command to evaluate. + cmd byte + // Expected value of instruction and Data pointer. + i, d int + // Expected values of *specific* cells; mapping between cell + // index and value expected there. Since the cell region is + // large (3e4) and sparse (0 by default) only specific cells + // are checked. + cells map[int]byte + // Expected output data bytes. + out []byte + }{ + // Check < and > move data pointer around circular cell region. + {'<', 1, 29999, nil, nil}, + {'>', 2, 0, nil, nil}, + // Check that + and - modify cell values. + {'+', 3, 0, map[int]byte{0: 1}, nil}, + {'-', 4, 0, map[int]byte{0: 0}, nil}, + // Check that , and . read input and write output bytes. + {',', 5, 0, map[int]byte{0: 4}, nil}, + {'>', 6, 1, map[int]byte{0: 4}, nil}, + {',', 7, 1, map[int]byte{0: 4, 1: 8}, nil}, + {',', 8, 1, map[int]byte{0: 4, 1: 15}, nil}, + {'>', 9, 2, map[int]byte{0: 4, 1: 15}, nil}, + {',', 10, 2, map[int]byte{0: 4, 1: 15, 2: 16}, nil}, + {'.', 11, 2, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16}}, + {'<', 12, 1, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16}}, + {'.', 13, 1, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15}}, + {'<', 14, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15}}, + {'.', 15, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, + // Check that [ and ] are handled correctly by adding two adjacent cells. + // Evaluation is delayed in the presence of partial loops, so all internal + // state remain the same until the final ] is Eval'd. + {'[', 15, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, + {'-', 15, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, + {'>', 15, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, + {'+', 15, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, + {'<', 15, 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, + {']', 21, 0, map[int]byte{0: 0, 1: 19, 2: 16}, []byte{16, 15, 4}}, + {'>', 22, 1, map[int]byte{0: 0, 1: 19, 2: 16}, []byte{16, 15, 4}}, + {'.', 23, 1, map[int]byte{0: 0, 1: 19, 2: 16}, []byte{16, 15, 4, 19}}, + } + + var out bytes.Buffer + bf := New(bytes.NewBuffer([]byte{4, 8, 15, 16, 23, 41}), &out) + if err := checkInterpreter(bf, 0, 0, nil, nil); err != nil { + t.Fatalf("[0] Unexpected interpreter state: %v", err) + } + + for i, test := range tests { + if err := bf.Eval(test.cmd); err != nil { + t.Fatalf("[%d] Eval(%q) failed with error: %v", i+1, test.cmd, err) + } + if err := checkInterpreter(bf, test.i, test.d, test.cells, test.out); err != nil { + t.Errorf("[%d] Unexpected interpreter state: %v", i+1, err) + } + } +} + +func checkInterpreter(bf *Brainfuck, i, d int, cells map[int]byte, out []byte) error { + if bf.i != i { + return fmt.Errorf("instruction pointer; got %d, want %d", bf.i, i) + } + if bf.d != d { + return fmt.Errorf("data pointer; got %d, want %d", bf.d, d) + } + for idx, got := range bf.cells { + if want := cells[idx]; got != want { + return fmt.Errorf("cell value at index %d; got %d, want %d", idx, got, want) + } + } + if diff := cmp.Diff(out, bf.out.Bytes()); diff != "" { + return fmt.Errorf("output diff (-want +got): %s", diff) + } + return nil +} + +func TestInvalidLoopHandling(t *testing.T) { + invalidLoops := []string{"]", "[]]", "[][]]"} + for _, test := range invalidLoops { + _, err := Run(test, nil) + if err == nil { + t.Errorf("Parsed invalid program %q; expected error", test) + } + } +} + +func TestHelloWorld(t *testing.T) { + program := ` + >++++++++[<+++++++++>-]<. + >++++[<+++++++>-]<+. + +++++++.. + +++. + >>++++++[<+++++++>-]<++. + ------------. + >++++++[<+++++++++>-]<+. + <. + +++. + ------. + --------. + >>>++++[<++++++++>-]<+.` + + out, err := Run(program, nil) + if err != nil { + t.Fatalf("Run failed: %v", err) + } + if diff := cmp.Diff("Hello, World!", string(out)); diff != "" { + t.Errorf("Mismatched output data (-want +got):\n%s", diff) + } +} diff --git a/cmd/gbfy/main.go b/cmd/gbfy/main.go @@ -2,8 +2,10 @@ package main import ( "bufio" + "bytes" "flag" "fmt" + "io" "log" "os" @@ -19,8 +21,6 @@ var ( func main() { flag.Parse() - // Read contents of codeFile and inputFile to get commands - // to execute and any input data the program may expect. var ( err error cmds []byte @@ -39,51 +39,72 @@ func main() { } } - bf, err := gbfy.New(string(cmds), input) + var output bytes.Buffer + bf := gbfy.New(bytes.NewBuffer(input), &output) if err != nil { log.Fatalf("Failed to initialize interpreter: %v", err) } - output, err := bf.Run() - if err != nil { - log.Printf("Run failed with error: %v", err) + if len(cmds) > 0 { + for _, cmd := range cmds { + if err := bf.Eval(cmd); err != nil { + log.Fatalf("Failed to Eval command %q: %v", cmd, err) + } + } + log.Printf("Output from execution: %v\n", output.Bytes()) } - log.Printf("Output from execution: %q\n", output) - - // Drop user into interactive "REPL" if requested. Execution - // may continue in the REPL with whatever interpreter state - // state the above program left it in. if *launchREPL { - log.Println("... starting interactive REPL") - beInteractive(bf) + log.Println("Starting interactive REPL") + repl(bf) } } -const welcomeMsg = `Welcome! +const ( + welcomeMsg = `Welcome! :q[uit] to exit REPL loop :d[ump] to dump interpreter state :f[uck] to reset interpreter state` -func beInteractive(bf *gbfy.Brainfuck) { + prompt = "gbfy> " +) + +func repl(bf *gbfy.Brainfuck) { fmt.Println(welcomeMsg) in := bufio.NewReader(os.Stdin) for { - fmt.Print("gbfy> ") - line, _ := in.ReadString('\n') - if line[0] == ':' { - // Handle REPL commands. + fmt.Print(prompt) + line, err := in.ReadString('\n') + if err == io.EOF { + fmt.Println("") + return + } else if err != nil { + log.Fatalf("Error! %v", err) + } + + if len(line) > 0 && line[0] == ':' { switch line[1] { - case 'q': // exit REPL loop. - return - case 'd': // dump state to stdout. - fmt.Println(bf.String()) - case 'f': // reset interpreter. + case 'd': + replDump(bf) + case 'f': bf.Reset() - fmt.Println("Restarted interpreter...") + fmt.Println("Reset interpreter!") + case 'q': + return } } else { - // Handle a Brainfuck command(s). + for _, cmd := range []byte(line) { + if err := bf.Eval(cmd); err != nil { + log.Fatalf("Failed to Eval command %q: %v", cmd, err) + } + } } } } + +func replDump(bf *gbfy.Brainfuck) { + d, cells, i, cmds, out := bf.Dump() + fmt.Printf("Cells; d: %d, current cell value: %x\n", d, cells[d]) + fmt.Printf("Cmds: i: %d, current command: %q\n", i, cmds[i-1]) + fmt.Printf("Out: %v\n", out) +} diff --git a/gbfy.go b/gbfy.go @@ -1,141 +0,0 @@ -package gbfy - -import ( - "bytes" - "errors" - "fmt" - "strings" -) - -// Brainfuck is the interpreter state. -type Brainfuck struct { - cells [3e4]byte // cells - d int // data pointer for cells access - cmds []byte // program being executed - i int // instruction pointer for cmds access - loops map[int]int // stores index for matching [ or ] - in, out *bytes.Buffer // input and output buffers -} - -// New constructs a new Brainfuck interpreter with the given program -// and input buffer. -func New(program string, input []byte) (*Brainfuck, error) { - cmds, loops, err := parseProgram(program) - if err != nil { - return nil, err - } - return &Brainfuck{ - cmds: cmds, - loops: loops, - in: bytes.NewBuffer(input), - out: new(bytes.Buffer), - }, nil -} - -// Run the program loaded into the interpreter. -func (bf *Brainfuck) Run() ([]byte, error) { - for bf.i < len(bf.cmds) { - if err := bf.eval(bf.cmds[bf.i]); err != nil { - return nil, fmt.Errorf("Run failed with error: %v", err) - } - bf.i++ - } - return bf.out.Bytes(), nil -} - -// String dumps the Brainfuck interpreter state for debugging. -func (bf *Brainfuck) String() string { - var s strings.Builder - s.WriteString(fmt.Sprintf("pointers: [d=%d i=%d]\n", bf.d, bf.i)) - s.WriteString("non-zero cells: {\n") - // TODO: Better cell printing. - for i, c := range bf.cells { - if c != 0 { - s.WriteString(fmt.Sprintf(" %d: %d\n", i, c)) - } - } - s.WriteString("}") - return s.String() -} - -// Reset the interpreter state to defaults. -func (bf *Brainfuck) Reset() { - bf.cells = [3e4]byte{} - bf.i = 0 - bf.d = 0 - bf.cmds = nil -} - -// eval evaluates a single Brainfuck command. -func (bf *Brainfuck) eval(cmd byte) error { - switch cmd { - case '>': - // Increment data pointer. - bf.d++ - if bf.d >= len(bf.cells) { - bf.d -= len(bf.cells) - } - case '<': - // Decrement data pointer. - bf.d-- - if bf.d < 0 { - bf.d += len(bf.cells) - } - case '+': - // Increment value at current cell. - bf.cells[bf.d]++ - case '-': - // Decrement value at current cell. - bf.cells[bf.d]-- - case '.': - // Write current cell's value to output buffer. - bf.out.WriteByte(bf.cells[bf.d]) - case ',': - // Read value from input and store in current cell. - if b, err := bf.in.ReadByte(); err != nil { - return errors.New("program expects more input") - } else { - bf.cells[bf.d] = b - } - case '[': - // Loop start; jump to matching ] if current cell is zero. - if bf.cells[bf.d] == 0 { - bf.i = bf.loops[bf.i] - } - case ']': - // Loop end; jump to matching [ if current cell is non-zero. - if bf.cells[bf.d] != 0 { - bf.i = bf.loops[bf.i] - } - } - return nil -} - -func parseProgram(program string) ([]byte, map[int]int, error) { - cmds, opens, loops := []byte{}, []int{}, map[int]int{} - for _, cmd := range program { - switch cmd { - case '[', ']', '<', '>', '+', '-', ',', '.': - cmds = append(cmds, byte(cmd)) - } - } - for i, cmd := range cmds { - if cmd == '[' { - opens = append(opens, i) - } else if cmd == ']' { - if len(opens) == 0 { - return nil, nil, fmt.Errorf("mismatched [ at index %d", i) - } - // Remove matching [. - j := opens[len(opens)-1] - opens = opens[:len(opens)-1] - // Store mapping. - loops[i] = j - loops[j] = i - } - } - if len(opens) > 0 { - return nil, nil, errors.New("unclosed [ at end of program") - } - return cmds, loops, nil -} diff --git a/gbfy_test.go b/gbfy_test.go @@ -1,144 +0,0 @@ -package gbfy - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestEval(t *testing.T) { - tests := []struct { - // Command to evaluate. - cmd byte - // Expected value of Data pointer. - d int - // Expected values of *specific* cells; mapping between cell - // index and value expected there. Since the cell region is - // large (3e4) and sparse (0 by default) only specific cells - // are checked. - cells map[int]byte - // Expected output data. - out []byte - }{ - // Check < and > move data pointer around circular cell region. - {'<', 29999, nil, nil}, - {'>', 0, nil, nil}, - // Check that + and - modify cell values. - {'+', 0, map[int]byte{0: 1}, nil}, - {'-', 0, map[int]byte{0: 0}, nil}, - // Check that , and . read input and write output bytes. - // NB: output is inspected at the very end since the interpreter - // uses bytes.Buffer. - {',', 0, map[int]byte{0: 4}, nil}, - {'>', 1, map[int]byte{0: 4}, nil}, - {',', 1, map[int]byte{0: 4, 1: 8}, nil}, - {',', 1, map[int]byte{0: 4, 1: 15}, nil}, - {'>', 2, map[int]byte{0: 4, 1: 15}, nil}, - {',', 2, map[int]byte{0: 4, 1: 15, 2: 16}, nil}, - {'.', 2, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16}}, - {'<', 1, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16}}, - {'.', 1, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15}}, - {'<', 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15}}, - {'.', 0, map[int]byte{0: 4, 1: 15, 2: 16}, []byte{16, 15, 4}}, - // Hmm, we can't really test [ and ] with eval ... - } - - bf, _ := New("", []byte{4, 8, 15, 16, 23, 41}) - if err := checkInterpreter(bf, 0, 0, nil, nil); err != nil { - t.Fatalf("[0] Unexpected interpreter state: %v", err) - } - - for i, test := range tests { - if err := bf.eval(test.cmd); err != nil { - t.Fatalf("[%d] Eval(%q) failed with error: %v", i, test.cmd, err) - } - // Note since we called `eval` ourselves rather than `Run`, the instruction - // pointer is not being used, i.e. is always zero. It's tested separately below. - if err := checkInterpreter(bf, 0, test.d, test.cells, test.out); err != nil { - t.Errorf("[%d] Unexpected interpreter state: %v", i, err) - } - } -} - -func checkInterpreter(bf *Brainfuck, i, d int, cells map[int]byte, out []byte) error { - if bf.i != i { - return fmt.Errorf("instruction pointer; got %d, want %d", bf.i, i) - } - if bf.d != d { - return fmt.Errorf("data pointer; got %d, want %d", bf.d, d) - } - for idx, got := range bf.cells { - if want := cells[idx]; got != want { - return fmt.Errorf("cell value at index %d; got %b, want %b", idx, got, want) - } - } - if diff := cmp.Diff(out, bf.out.Bytes()); diff != "" { - return fmt.Errorf("output diff (-want +got): %s", diff) - } - return nil -} - -func TestParseProgram(t *testing.T) { - invalidLoops := []string{"]", "[]]", "[", "[]["} - for _, test := range invalidLoops { - _, err := New(test, nil) - if err == nil { - t.Errorf("Parsed invalid program %q; expected error", test) - } - } -} - -func TestLoops(t *testing.T) { - // Program computes sum of two adjanct cells, read from input. - program := ",>,[<+>-]<." - tests := []struct { - input, want []byte - }{ - {[]byte{3, 2}, []byte{5}}, - {[]byte{2, 3}, []byte{5}}, - {[]byte{3, 3}, []byte{6}}, - {[]byte{0, 0}, []byte{0}}, - } - for _, test := range tests { - bf, err := New(program, test.input) - if err != nil { - t.Fatalf("New failed: %v", err) - } - out, err := bf.Run() - if err != nil { - t.Fatalf("Run failed: %v", err) - } - if diff := cmp.Diff(test.want, out); diff != "" { - t.Errorf("Mismatched output data (-want +got):\n%s", diff) - } - } -} - -func TestHelloWorld(t *testing.T) { - program := ` - >++++++++[<+++++++++>-]<. - >++++[<+++++++>-]<+. - +++++++.. - +++. - >>++++++[<+++++++>-]<++. - ------------. - >++++++[<+++++++++>-]<+. - <. - +++. - ------. - --------. - >>>++++[<++++++++>-]<+.` - - bf, err := New(program, nil) - if err != nil { - t.Fatalf("New failed: %v", err) - } - out, err := bf.Run() - if err != nil { - t.Fatalf("Run failed: %v", err) - } - if diff := cmp.Diff("Hello, World!", string(out)); diff != "" { - t.Errorf("Mismatched output data (-want +got):\n%s", diff) - } -}