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 a2f90706e8525a8187835bc566f3f22b378c6c27
parent f5c1fb462a5163db0e9240fa5cad6a67bd4b2a61
Author: david cochran <about.trout@gmail.com>
Date:   Sat, 30 Dec 2023 19:48:02 -0800

fixes #7 - move partial loop logic into recursive eval

Diffstat:
Mbf.go | 136++++++++++++++++++++++++++++++++++---------------------------------------------
Mbf_test.go | 38++++++++++++++++++++++++++++++++++++++
2 files changed, 96 insertions(+), 78 deletions(-)

diff --git a/bf.go b/bf.go @@ -1,21 +1,17 @@ package gbfy import ( - "bytes" "errors" "fmt" "io" ) 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 io.Reader - out io.Writer - + cells [3e4]byte + cmds []byte + i, d int // instruction and data pointer + in io.Reader // for reading with , + out io.Writer // for writing with . loops map[int]int // stores index for matching [ or ] parLoops []int // indices for not-yet-complete loops } @@ -25,93 +21,77 @@ func New(in io.Reader, out io.Writer) *Brainfuck { return &Brainfuck{in: in, out: out, loops: map[int]int{}} } -// Eval evaluates a single command with the given interpreter. +// Eval a single Brainfuck command. 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 } + // Handle loops before calling internal eval. + if cmd == '[' { + bf.parLoops = append(bf.parLoops, len(bf.cmds)-1) + } else if cmd == ']' { + if len(bf.parLoops) == 0 { + return errors.New("invalid loop close") + } + i, j := len(bf.cmds)-1, bf.parLoops[len(bf.parLoops)-1] + bf.loops[i] = j + bf.loops[j] = i + bf.parLoops = bf.parLoops[:len(bf.parLoops)-1] + } + if len(bf.parLoops) > 0 { + return nil // delay eval if there are partial loops. + } + return bf.eval() } func (bf *Brainfuck) eval() error { - if len(bf.parLoops) > 0 { - // Delay evaluation if there are unclosed loops. + if n := len(bf.cmds); n == 0 || n <= bf.i { 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 '.': - if _, err := bf.out.Write([]byte{bf.cells[bf.d]}); err != nil { - return fmt.Errorf("failed to Write output: %v", err) - } - case ',': - input := make([]byte, 1) - if _, err := bf.in.Read(input); err != nil { - return fmt.Errorf("failed to Read input: %v", err) - } else { - bf.cells[bf.d] = input[0] - } - 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] - } + 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 '.': + if _, err := bf.out.Write([]byte{bf.cells[bf.d]}); err != nil { + return fmt.Errorf("failed to Write output: %v", err) + } + case ',': + input := make([]byte, 1) + if _, err := bf.in.Read(input); err != nil { + return fmt.Errorf("failed to Read input: %v", err) + } + bf.cells[bf.d] = input[0] + 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 + + bf.i++ + return bf.eval() } // Dump interpreter state to caller. func (bf *Brainfuck) Dump() (int, []byte, int, []byte) { return bf.d, bf.cells[:], bf.i, bf.cmds } - -// Run runs a Brainfuck program and returns output bytes or error. -func Run(cmds []byte, in io.Reader) ([]byte, error) { - var out bytes.Buffer - bf := New(in, &out) - for _, cmd := range cmds { - if err := bf.Eval(cmd); err != nil { - return nil, err - } - } - return out.Bytes(), nil -} diff --git a/bf_test.go b/bf_test.go @@ -3,6 +3,7 @@ package gbfy import ( "bytes" "fmt" + "io" "testing" "github.com/google/go-cmp/cmp" @@ -119,3 +120,40 @@ func TestHelloWorld(t *testing.T) { t.Errorf("Mismatched output data (-want +got):\n%s", diff) } } + +func TestCellWrapping(t *testing.T) { + bf := New(nil, nil) + // Move to the right .... 3e4 - 1 times. + for i := 0; i < len(bf.cells)-1; i++ { + if err := bf.Eval('>'); err != nil { + t.Fatalf("Eval(>) failed with error %v", err) + } + } + if err := checkInterpreter(bf, 29999, 29999, nil); err != nil { + t.Fatalf("Unexpected interpreter state: %v", err) + } + // Move to the right once more -- should wrap! + if err := bf.Eval('>'); err != nil { + t.Fatalf("Eval(>) failed with error %v", err) + } else if err := checkInterpreter(bf, 30000, 0, nil); err != nil { + t.Fatalf("Unexpected interpreter state: %v", err) + } + // Move to the left -- should wrap! + if err := bf.Eval('<'); err != nil { + t.Fatalf("Eval(<) failed with error %v", err) + } + if err := checkInterpreter(bf, 30001, 29999, nil); err != nil { + t.Fatalf("Unexpected interpreter state: %v", err) + } +} + +func Run(cmds []byte, in io.Reader) ([]byte, error) { + var out bytes.Buffer + bf := New(in, &out) + for _, cmd := range cmds { + if err := bf.Eval(cmd); err != nil { + return nil, err + } + } + return out.Bytes(), nil +}