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 afc096e93c59690b90ddb4dc6b3bdeb6f4e7e31f
Author: david cochran <about.trout@gmail.com>
Date:   Sun,  8 Jan 2023 21:38:54 +0000

initial commit

Diffstat:
AREADME.md | 3+++
Agbfy.go | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agbfy_test.go | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 5+++++
Ago.sum | 2++
5 files changed, 218 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -0,0 +1,3 @@ +# `gbfy` + +A [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) interpreter in Golang. diff --git a/gbfy.go b/gbfy.go @@ -0,0 +1,128 @@ +package gbfy + +import ( + "bytes" + "errors" + "fmt" +) + +// Brainfuck is the interpreter state. +type Brainfuck struct { + cells [3e4]byte // cells/tape + cmds []byte // program being executed + d, i int // data and instruction pointers + 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 { + return &Brainfuck{ + cmds: []byte(program), + in: bytes.NewBuffer(input), + out: new(bytes.Buffer), + } +} + +// 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 +} + +// 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. Continue through loop body if current cell + // is non-zero, otehrwise jump to matching ]. + if bf.cells[bf.d] == 0 { + idx, err := findMatchingClose(bf.cmds, bf.i) + if err != nil { + return fmt.Errorf("Invalid program! %v", err) + } + bf.i = idx + } + case ']': + // Loop end. Jump to matching [ if current cell is non-zero. + if bf.cells[bf.d] != 0 { + idx, err := findMatchingOpen(bf.cmds, bf.i) + if err != nil { + return fmt.Errorf("Invalid program! %v", err) + } + bf.i = idx + } + default: + // TODO: Consider stripping non-brainfuck characters in pre-processing. + // Technically they are valid in BF programs and should be treated as comments. + return fmt.Errorf("Invalid program! Unknown command %q", cmd) + } + return nil +} + +func findMatchingClose(cmds []byte, i int) (int, error) { + var opens int + for i < len(cmds) { + switch cmds[i] { + case '[': + opens++ + case ']': + if opens == 0 { + return i, nil + } + opens-- + } + i++ + } + return 0, fmt.Errorf("Missing matching ] for [ at %d", i) +} + +func findMatchingOpen(cmds []byte, i int) (int, error) { + var closes int + for i >= 0 { + switch cmds[i] { + case '[': + if closes == 0 { + return i, nil + } + closes-- + case ']': + closes++ + } + i-- + } + return 0, fmt.Errorf("Missing matching [ for ] at %d", i) +} diff --git a/gbfy_test.go b/gbfy_test.go @@ -0,0 +1,80 @@ +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 +} diff --git a/go.mod b/go.mod @@ -0,0 +1,5 @@ +module github.com/abtrout/gbfy + +go 1.19 + +require github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=