initial commit; we have evaluation!
This commit is contained in:
commit
4dc64947db
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
prosper
|
||||||
|
.DS_Store
|
186
builtins.go
Normal file
186
builtins.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
94
eval.go
Normal file
94
eval.go
Normal file
@ -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
|
||||||
|
}
|
53
main.go
Normal file
53
main.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
42
mem.go
Normal file
42
mem.go
Normal file
@ -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
|
||||||
|
}
|
69
mem_test.go
Normal file
69
mem_test.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
stack.go
Normal file
23
stack.go
Normal file
@ -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...)
|
||||||
|
}
|
44
stack_test.go
Normal file
44
stack_test.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
33
words.go
Normal file
33
words.go
Normal file
@ -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
|
||||||
|
}
|
22
words_test.go
Normal file
22
words_test.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user