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:
| M | bf.go | | | 136 | ++++++++++++++++++++++++++++++++++--------------------------------------------- |
| M | bf_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
+}