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 61f8144755065e465703f2381b0c07e632dda094
parent bfdbf53a13e919d0c466180ed4a5819a2f659e1d
Author: david cochran <about.trout@gmail.com>
Date:   Fri, 29 Dec 2023 09:36:30 -0800

refactor to use io Reader/Writer

Diffstat:
Mbf.go | 45+++++++++++++++++++++++++--------------------
Mbf_test.go | 25++++++++++++-------------
Mcmd/gbfy/main.go | 54+++++++++++++++++++++++++++---------------------------
3 files changed, 64 insertions(+), 60 deletions(-)

diff --git a/bf.go b/bf.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" ) type Brainfuck struct { @@ -12,29 +13,18 @@ type Brainfuck struct { cmds []byte // commands evaluated by the interpreter i int // instruction pointer for cmds access - in, out *bytes.Buffer // input and output buffers + in io.Reader + out io.Writer 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 { +func New(in io.Reader, out io.Writer) *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 { @@ -85,12 +75,15 @@ func (bf *Brainfuck) eval() error { case '-': bf.cells[bf.d]-- case '.': - bf.out.WriteByte(bf.cells[bf.d]) + if _, err := bf.out.Write([]byte{bf.cells[bf.d]}); err != nil { + return fmt.Errorf("failed to Write output: %v", err) + } case ',': - if b, err := bf.in.ReadByte(); err != nil { - return fmt.Errorf("failed to readDataVal: %v", err) + 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] = b + bf.cells[bf.d] = input[0] } case '[': if bf.cells[bf.d] == 0 { @@ -107,8 +100,8 @@ func (bf *Brainfuck) eval() error { } // 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() +func (bf *Brainfuck) Dump() (int, []byte, int, []byte) { + return bf.d, bf.cells[:], bf.i, bf.cmds } // Reset interpreter state. @@ -122,3 +115,15 @@ func (bf *Brainfuck) Reset() { bf.loops = map[int]int{} bf.parLoops = nil } + +// 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 @@ -55,21 +55,24 @@ func TestEval(t *testing.T) { 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 { + if err := checkInterpreter(bf, 0, 0, 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) + t.Fatalf("[%d] Eval(%q) failed with error: %v", i, 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) + if err := checkInterpreter(bf, test.i, test.d, test.cells); err != nil { + t.Fatalf("[%d] Unexpected interpreter state: %v", i, err) + } + if diff := cmp.Diff(test.out, out.Bytes()); diff != "" { + t.Fatalf("[%d] output diff (-want +got): %s", i, diff) } } } -func checkInterpreter(bf *Brainfuck, i, d int, cells map[int]byte, out []byte) error { +func checkInterpreter(bf *Brainfuck, i, d int, cells map[int]byte) error { if bf.i != i { return fmt.Errorf("instruction pointer; got %d, want %d", bf.i, i) } @@ -81,17 +84,13 @@ func checkInterpreter(bf *Brainfuck, i, d int, cells map[int]byte, out []byte) e 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 { + tests := []string{"]", "[]]", "[][]]"} + for _, test := range tests { + if _, err := Run([]byte(test), nil); err == nil { t.Errorf("Parsed invalid program %q; expected error", test) } } @@ -112,7 +111,7 @@ func TestHelloWorld(t *testing.T) { --------. >>>++++[<++++++++>-]<+.` - out, err := Run(program, nil) + out, err := Run([]byte(program), nil) if err != nil { t.Fatalf("Run failed: %v", err) } diff --git a/cmd/gbfy/main.go b/cmd/gbfy/main.go @@ -21,49 +21,55 @@ var ( func main() { flag.Parse() - var ( - input []byte - output bytes.Buffer - ) + var in, out bytes.Buffer if *dataFile != "" { - var err error - if input, err = os.ReadFile(*dataFile); err != nil { + if input, err := os.ReadFile(*dataFile); err != nil { log.Fatalf("Failed to read input file: %v", err) + } else { + in = *bytes.NewBuffer(input) } } + bf := gbfy.New(&in, &out) - bf := gbfy.New(bytes.NewBuffer(input), &output) if *codeFile != "" { if err := runCodeFile(bf); err != nil { log.Fatalf("Execution failed: %v", err) } - } else if err := runPiped(bf); err != nil { - log.Fatalf("Execution failed: %v", err) + log.Printf("Output from execution: %v\n", out.Bytes()) + } else { + if err := runPiped(bf); err != nil { + log.Fatalf("Execution failed: %v", err) + } + log.Printf("Output from execution: %v\n", out.Bytes()) } if *endInteractive { log.Println("Starting interactive REPL ...") - repl(bf) + repl(bf, &out) } } const welcomeMsg = `Go Brainfuck Yourself! - :q[uit] to exit REPL loop :d[ump] to dump interpreter state - :f[uck] to reset interpreter state` + :f[uck] to reset interpreter state + :q[uit] to exit REPL loop` -func repl(bf *gbfy.Brainfuck) { +func repl(bf *gbfy.Brainfuck, out *bytes.Buffer) { fmt.Println(welcomeMsg) f, err := os.Open("/dev/tty") if err != nil { log.Fatalf("Failed to open TTY: %v", err) } - in := bufio.NewReader(f) + stdIn := bufio.NewReader(f) + + // TODO: Switch interpreter input/output here? Continue + // reading from original `input` (i.e. from dataFile) + // until EOF -- then switch to TTY? for { fmt.Print("gbfy> ") - line, err := in.ReadBytes('\n') + line, err := stdIn.ReadBytes('\n') if err == io.EOF { break } else if err != nil { @@ -73,7 +79,7 @@ func repl(bf *gbfy.Brainfuck) { if len(line) > 0 && line[0] == ':' { switch line[1] { case 'd': - formatDump(bf.Dump()) + formatDump(bf, out.Bytes()) case 'f': bf.Reset() fmt.Println("Reset interpreter!") @@ -98,7 +104,6 @@ func runCodeFile(bf *gbfy.Brainfuck) error { if err := runCommands(bf, in); err != nil { return err } - logOutput(bf) return nil } @@ -113,7 +118,6 @@ func runPiped(bf *gbfy.Brainfuck) error { return err } } - logOutput(bf) return nil } @@ -132,13 +136,9 @@ func runCommands(bf *gbfy.Brainfuck, r *bufio.Reader) error { return nil } -func logOutput(bf *gbfy.Brainfuck) { - _, _, _, _, out := bf.Dump() - log.Printf("Output from execution: %v\n", out) -} - -func formatDump(d int, cells []byte, i int, cmds []byte, out []byte) { - 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) +func formatDump(bf *gbfy.Brainfuck, out []byte) { + d, cells, i, cmds := bf.Dump() + fmt.Printf("CELLS d: %3d, current cell value: %x\n", d, cells[d]) + fmt.Printf("CMDS i: %3d, current command: %q\n", i, cmds[i-1]) + fmt.Printf("OUT %v\n", out) }