From 4dc64947dbdf2c1d6541e5ccf8d5c46a6b87d6dc Mon Sep 17 00:00:00 2001 From: David Ashby Date: Sat, 13 Feb 2021 21:09:56 -0500 Subject: [PATCH] initial commit; we have evaluation! --- .gitignore | 2 + builtins.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++ eval.go | 94 +++++++++++++++++++++++++ go.mod | 3 + main.go | 53 ++++++++++++++ mem.go | 42 ++++++++++++ mem_test.go | 69 +++++++++++++++++++ stack.go | 23 +++++++ stack_test.go | 44 ++++++++++++ words.go | 33 +++++++++ words_test.go | 22 ++++++ 11 files changed, 571 insertions(+) create mode 100644 .gitignore create mode 100644 builtins.go create mode 100644 eval.go create mode 100644 go.mod create mode 100644 main.go create mode 100644 mem.go create mode 100644 mem_test.go create mode 100644 stack.go create mode 100644 stack_test.go create mode 100644 words.go create mode 100644 words_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d6c63b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +prosper +.DS_Store \ No newline at end of file diff --git a/builtins.go b/builtins.go new file mode 100644 index 0000000..f6d7759 --- /dev/null +++ b/builtins.go @@ -0,0 +1,186 @@ +package main + +import "fmt" + +// Builtins is a handy holder for our various default words +type Builtins struct{} + +// Add sums the top two numbers on the stack and pushes the result +func (b *Builtins) Add(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + s.Push(r1 + r2) + return nil + } +} + +// Sub performs NOS - TOS and pushes the result +func (b *Builtins) Sub(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + s.Push(r2 - r1) + return nil + } +} + +// Mul multiplies the two numbers on the top of the stack and pushes the result +func (b *Builtins) Mul(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + s.Push(r2 * r1) + return nil + } +} + +// Div performs NOS/TOS and pushes the (integer!) result +func (b *Builtins) Div(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + s.Push(r2 / r1) + return nil + } +} + +// Print pops the stack and outputs it to stdout +func (b *Builtins) Print(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + fmt.Print(r1, " ") + return nil + } +} + +// Dup pops the stack, then pushes two copies onto the stack +func (b *Builtins) Dup(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + s.Push(r1) + s.Push(r1) + return nil + } +} + +// Swap inverts the order of TOS and NOS +func (b *Builtins) Swap(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + s.Push(r1) + s.Push(r2) + return nil + } +} + +// Over duplicates NOS to TOS, resulting in NOS TOS NOS +func (b *Builtins) Over(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + s.Push(r2) + s.Push(r1) + s.Push(r2) + return nil + } +} + +// Drop simply discards TOS +func (b *Builtins) Drop(s *Stack) func() error { + return func() error { + _, err := s.Pop() + return err + } +} + +// Rot cycles the first three items on the stack: TOS 1 2 3 -> TOS 3 1 2 +func (b *Builtins) Rot(s *Stack) func() error { + return func() error { + r1, err := s.Pop() + if err != nil { + return err + } + r2, err := s.Pop() + if err != nil { + return err + } + r3, err := s.Pop() + if err != nil { + return err + } + s.Push(r2) + s.Push(r1) + s.Push(r3) + return err + } +} + +// Words outputs a list of all known words in the dictionary +func (b *Builtins) Words(d Dictionary) func() error { + return func() error { + for n := range d { + fmt.Printf("%s ", n) + } + return nil + } +} + +// CR prints a newline to standard out +func (b *Builtins) CR() func() error { + return func() error { + fmt.Print("\n") + return nil + } +} + +// Debug prints the stack without modifying it +func (b *Builtins) Debug(s *Stack) func() error { + return func() error { + fmt.Print(s.values, " ") + return nil + } +} diff --git a/eval.go b/eval.go new file mode 100644 index 0000000..a1d1ad7 --- /dev/null +++ b/eval.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "strconv" + "strings" +) + +// Context is a set of Dictionary + Stack representing a runtime environment +type Context struct { + Dictionary Dictionary + Stack *Stack +} + +// Eval evaulates a given line, recursively descending into given words as needed +func (c *Context) Eval(line string) error { + // state + var word []byte + var comment bool + immediate := true + + for i := 0; i < len(line); i = i + 1 { + switch line[i] { + case '(', ')': // comments + if len(word) == 0 { + if line[i] == '(' { + comment = true + continue + } + comment = false + continue + } else { + word = append(word, line[i]) + } + case ':', ';': // COMPILE/IMMEDIATE mode swapping + if len(word) == 0 { + if line[i] == ':' { + immediate = false + } + } else { + if line[i-1] == ' ' && line[i] == ';' { + def := strings.SplitN(strings.TrimSpace(string(word)), " ", 2) + c.Dictionary.AddWord(def[0], nil, def[1]+" ") + word = []byte{} + immediate = true + } else { + word = append(word, line[i]) + } + } + case ' ': + if !immediate { // continue building our subroutine if we're not in immediate mode... + word = append(word, line[i]) + continue + } + if len(word) == 0 || comment { + // empty space, just continue... + continue + } + int, err := strconv.Atoi(string(word)) + if err == nil { + // it was a number! put it on the stack. + c.Stack.Push(int) + word = []byte{} + continue + } + // it wasn't a number. Is it a word we know? + w, err := c.Dictionary.GetWord(string(word)) + if err != nil { + return fmt.Errorf("could not parse %s; %v", w.Name, err) + } + if w.Impl != nil { + // we have an implementation for that word. Run it. + err := w.Impl() + if err != nil { + return err + } + word = []byte{} + } else if w.Source != "" { + // user-defined word; let's descend... + err := c.Eval(w.Source) + if err != nil { + return err + } + word = []byte{} + } + default: + if !comment { + word = append(word, line[i]) + } + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13f5067 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.yetaga.in/alazyreader/prosper + +go 1.15 diff --git a/main.go b/main.go new file mode 100644 index 0000000..bfa8270 --- /dev/null +++ b/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func main() { + stack := Stack{values: []int{}} + + dict := Dictionary{} + b := &Builtins{} + dict.AddWord("+", b.Add(&stack), "") + dict.AddWord("-", b.Sub(&stack), "") + dict.AddWord("*", b.Mul(&stack), "") + dict.AddWord("/", b.Div(&stack), "") + dict.AddWord(".", b.Print(&stack), "") + dict.AddWord("DUP", b.Dup(&stack), "") + dict.AddWord("SWAP", b.Swap(&stack), "") + dict.AddWord("OVER", b.Over(&stack), "") + dict.AddWord("DROP", b.Drop(&stack), "") + dict.AddWord("ROT", b.Rot(&stack), "") + dict.AddWord("WORDS", b.Words(dict), "") + dict.AddWord(".S", b.Debug(&stack), "") + dict.AddWord("CR", b.CR(), "") + + c := Context{ + Dictionary: dict, + Stack: &stack, + } + + reader := bufio.NewReader(os.Stdin) + fmt.Print("prosper") + + // read loop + for { + fmt.Print("\n> ") + + line, err := reader.ReadString('\n') + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + line = strings.TrimSpace(line) + err = c.Eval(line + " ") // append a space to make sure we always close out our parse loop + if err != nil { + fmt.Printf("error in evaluation: %v\n", err) + } + fmt.Print("ok") + } +} diff --git a/mem.go b/mem.go new file mode 100644 index 0000000..190341e --- /dev/null +++ b/mem.go @@ -0,0 +1,42 @@ +package main + +import "fmt" + +// Memory provides an addressable map of integer cells +type Memory struct { + intern map[int]int + nextFree int +} + +// Read takes a starting address and a count of cells to read after it; +// (0, 2) reads the first two cells out of memory, for example. +func (m *Memory) Read(addr int, count int) []int { + r := []int{} + for i := 0; i < count; i++ { + r = append(r, m.intern[addr+i]) + } + return r +} + +// Write inserts the given values into memory, overwriting any existing contents, +// starting at the provided memory address and incrementing upward. +func (m *Memory) Write(addr int, values []int) error { + if addr < 0 { + return fmt.Errorf("addr out of range") + } + for i := range values { + m.intern[addr+i] = values[i] + } + // we've written past our marker, note that + if m.nextFree < addr+len(values) { + m.nextFree = addr + len(values) + } + return nil +} + +// NextFreeAddress provides the furthest "out" address, beyond which is uninitialized memory. +// Old memory is never "reclaimed", even if the program manually 0s it out. +// If you want to build your own GC, knock yourself out. +func (m *Memory) NextFreeAddress() int { + return m.nextFree +} diff --git a/mem_test.go b/mem_test.go new file mode 100644 index 0000000..a9aba37 --- /dev/null +++ b/mem_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "testing" +) + +func TestMemWrite(t *testing.T) { + m := Memory{ + intern: make(map[int]int), + } + + // write in some memory, not starting at the head + err := m.Write(1, []int{1, 2, 3}) + if err != nil { + t.Log(err.Error()) + t.Fail() + } + // only three values in memory + if len(m.intern) != 3 { + t.Fail() + } + if m.NextFreeAddress() != 4 { + t.Log("expected nextFree to be 4, got", m.nextFree) + t.Fail() + } + + // can't write to negative addresses + if m.Write(-1, []int{1}) == nil { + t.Fail() + } +} + +func TestMemRead(t *testing.T) { + m := Memory{ + intern: map[int]int{0: 1, 1: 2, 2: 3, 100: 101}, + } + + // Read two known locations + r := m.Read(0, 2) + if len(r) != 2 { + t.Fail() + } + if r[0] != 1 || r[1] != 2 { + t.Fail() + } + + // Read past known memory + r2 := m.Read(100, 2) + if len(r2) != 2 { + t.Fail() + } + if r2[0] != 101 || r2[1] != 0 { + t.Fail() + } + + // read empty memory + m2 := Memory{ + intern: map[int]int{}, + } + r3 := m2.Read(0, 100) + if len(r3) != 100 { + t.Fail() + } + for i := range r3 { + if r3[i] != 0 { + t.Fail() + } + } +} diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..379c716 --- /dev/null +++ b/stack.go @@ -0,0 +1,23 @@ +package main + +import "fmt" + +// Stack is a stack of integers with no defined max depth +type Stack struct { + values []int +} + +// Pop returns the top of the stack +func (s *Stack) Pop() (int, error) { + if len(s.values) == 0 { + return 0, fmt.Errorf("stack empty") + } + i := s.values[0] + s.values = s.values[1:] + return i, nil +} + +// Push adds a value to the top of the stack +func (s *Stack) Push(i int) { + s.values = append([]int{i}, s.values...) +} diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..922503f --- /dev/null +++ b/stack_test.go @@ -0,0 +1,44 @@ +package main + +import "testing" + +func TestStackPop(t *testing.T) { + s := Stack{ + values: []int{1, 2, 3}, + } + i, err := s.Pop() + if err != nil { + t.Log(err.Error()) + t.Fail() + } + if i != 1 { + t.Fail() + } + if len(s.values) != 2 { + t.Fail() + } + s = Stack{ + values: []int{}, + } + i, err = s.Pop() + if err == nil { + t.Log("expected error") + t.Fail() + } + if i != 0 { + t.Fail() + } +} + +func TestStackPush(t *testing.T) { + s := Stack{ + values: []int{1, 2, 3}, + } + s.Push(4) + if len(s.values) != 4 { + t.Fail() + } + if s.values[0] != 4 || s.values[1] != 1 { + t.Fail() + } +} diff --git a/words.go b/words.go new file mode 100644 index 0000000..d5fc43c --- /dev/null +++ b/words.go @@ -0,0 +1,33 @@ +package main + +import "fmt" + +// Dictionary is a simple map of names to words +type Dictionary map[string]Word + +// A Word defines a subroutine +type Word struct { + Name string + Impl func() error + Source string +} + +// AddWord inserts a new word into the dictonary, optionally overwriting the existing word +func (d Dictionary) AddWord(name string, impl func() error, source string) { + d[name] = Word{ + Name: name, + Impl: impl, + Source: source, + } +} + +// GetWord returns a word from the dictionary or an error if it's undefined +func (d Dictionary) GetWord(name string) (Word, error) { + w, ok := d[name] + if !ok { + return Word{ + Name: name, + }, fmt.Errorf("no word found") + } + return w, nil +} diff --git a/words_test.go b/words_test.go new file mode 100644 index 0000000..0ac07a0 --- /dev/null +++ b/words_test.go @@ -0,0 +1,22 @@ +package main + +import "testing" + +func TestDictionary(t *testing.T) { + d := Dictionary{} + + d.AddWord("INC", nil, "1 + ") + w, err := d.GetWord("INC") + if err != nil { + t.Fail() + } + if w.Name != "INC" { + t.Fail() + } + if w.Impl != nil { + t.Fail() + } + if w.Source != "1 +" { + t.Fail() + } +}