Skip to content

Commit

Permalink
Hashing object system (#15)
Browse files Browse the repository at this point in the history
* feat(hash): hash key

* feat(hash): hash inspect function

* feat(eval): hash

* doc(README): improve readme with examples

* refactor(example): move monkey.rs to .monkey
  • Loading branch information
vit0rr authored Oct 11, 2024
1 parent 2858ce0 commit 4575770
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 22 deletions.
101 changes: 99 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,105 @@ It is based on the book [Writing An Interpreter In Go](https://interpreterbook.c
## Instructions to run
```bash
$ go run main.go
# or if you want to run a file
$ go run main.go <filename>.monkey
```

## Example
TODO
## Features
- [x] Mathematical expressions
- [x] Variable bindings
- [x] functions
- [x] conditionals
- [x] return statements
- [x] higher-order functions
- [x] closures
- [x] integers
- [x] booleans
- [x] strings
- [x] arrays
- [x] hashes


## Examples:
### Church Encoding
```rust
let to_integer = fn(proc) {
return proc(fn(x) { return x + 1 })(0)
};

let ZERO = fn(f) { fn(x) { x } };
let ONE = fn(f) { fn(x) { f(x) } };
let TWO = fn(f) { fn(x) { f(f(x)) } };
let THREE = fn(f) { fn(x) { f(f(f(x))) } };

let EXP = fn(m) { fn(n) { m(n) } };
let SUCC = fn(n) { fn(f) { fn(x) { f(n(f)(x)) } } };


puts(to_integer(TWO));
puts("succ one: ", to_integer(SUCC(ONE)));
puts("exp two three: ", to_integer(EXP(TWO)(THREE)));
puts("number 10: ", to_integer(fn(f) { fn(x) { f(f(f(f(f(f(f(f(f(f(x)))))))))) } }));
```

### Fibonacci
```rust
let fibonacci = fn(x) {
if (x == 0) {
return 0;
} else {
if (x == 1) {
return 1;
} else {
fibonacci(x - 1) + fibonacci(x - 2);
}
}
};

let result = fibonacci(10);
puts(result); // 55
```

### higher-order functions
```rust
let map = fn(arr, f) {
let iter = fn(arr, accumulated) {
if (len(arr) == 0) {
accumulated
} else {
iter(rest(arr), push(accumulated, f(first(arr))))
}
};
iter(arr, []);
};

let reduce = fn(arr, initial, f) {
let iter = fn(arr, result) {
if (len(arr) == 0) {
result
} else {
iter(rest(arr), f(result, first(arr)))
}
};
iter(arr, initial)
};

let doubled = map([1, 2, 3, 4, 5], fn(x) {
return x * 2
});
puts((doubled)); // [2, 4, 6, 8, 10]

let sum = reduce([1, 2, 3, 4, 5], 0, fn(acc, value) {
return acc + value
});
puts(sum); // 15
```

### Closures
```rust
let add = fn(a, b) { a + b; };
let addTwo = fn(a) { add(a, 2); };
let addThree = fn(a) { add(a, 3); };
let applyFunc = fn(a, b, func) { func(a, b); };
applyFunc(3, 4, add); // 7
```
18 changes: 1 addition & 17 deletions code.monkey
Original file line number Diff line number Diff line change
@@ -1,17 +1 @@
let to_integer = fn(proc) { proc(fn(x) { x + 1 })(0) };

let ZERO = fn(f) { fn(x) { x } };
let ONE = fn(f) { fn(x) { f(x) } };
let TWO = fn(f) { fn(x) { f(f(x)) } };
let THREE = fn(f) { fn(x) { f(f(f(x))) } };

let TRUE = fn(x) { fn(y) { x } };
let FALSE = fn(x) { fn(y) { y } };

let EXP = fn(m) { fn(n) { m(n) } };
let SUCC = fn(n) { fn(f) { fn(x) { f(n(f)(x)) } } };

puts(to_integer(TWO));
puts("succ one: ", to_integer(SUCC(ONE)));
puts("exp two three: ", to_integer(EXP(TWO)(THREE)));
puts("number 10: ", to_integer(fn(f) { fn(x) { f(f(f(f(f(f(f(f(f(f(x)))))))))) } }));
puts("Hello World!")
47 changes: 46 additions & 1 deletion evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
case *ast.StringLiteral:
return &object.String{Value: node.Value}

case *ast.HashLiteral:
return evalHashLiteral(node, env)

case *ast.Boolean:
return nativeBoolToBooleanObject(node.Value)

Expand Down Expand Up @@ -114,16 +117,58 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
return nil
}

func evalHashLiteral(node *ast.HashLiteral, env *object.Environment) object.Object {
pairs := make(map[object.HashKey]object.HashPair)

for keyNode, valueNode := range node.Pairs {
key := Eval(keyNode, env)
if isError(key) {
return key
}

hashKey, ok := key.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", key.Type())
}

value := Eval(valueNode, env)
if isError(value) {
return value
}

hashed := hashKey.HashKey()
pairs[hashed] = object.HashPair{Key: key, Value: value}
}

return &object.Hash{Pairs: pairs}
}

func evalIndexExpression(left, index object.Object) object.Object {
switch {
case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
return evalArrayIndexExpression(left, index)

case left.Type() == object.HASH_OBJ:
return evalHashIndexExpression(left, index)
default:
return newError("index operator not supported: %s", left.Type())
}
}

func evalHashIndexExpression(hash, index object.Object) object.Object {
hashObject := hash.(*object.Hash)
key, ok := index.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", index.Type())
}

pair, ok := hashObject.Pairs[key.HashKey()]
if !ok {
return NULL
}

return pair.Value
}

func evalArrayIndexExpression(array, index object.Object) object.Object {
arrayObject := array.(*object.Array)
idx := index.(*object.Integer).Value
Expand Down
67 changes: 67 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func TestErrorHandling(t *testing.T) {
`, "unknown operator: BOOLEAN + BOOLEAN"},
{"foobar", "identifier not found: foobar"},
{`"Hello" - "World"`, "unknown operator: STRING - STRING"},
{`{"name": "Monkey"}[fn(x) { x }];`, "unusable as hash key: FUNCTION"},
}

for _, tt := range tests {
Expand Down Expand Up @@ -378,6 +379,72 @@ func TestArrayIndexExpressions(t *testing.T) {
}
}

func TestHashLiterals(t *testing.T) {
input := `let two = "two";
{
"one": 10 - 9,
two: 1 + 1,
"thr" + "ee": 6 / 2,
4: 4,
true: 5,
false: 6
}`

evaluated := testEval(input)
result, ok := evaluated.(*object.Hash)
if !ok {
t.Fatalf("object is not Hash. got=%T (%+v)", evaluated, evaluated)
}

expected := map[object.HashKey]int64{
(&object.String{Value: "one"}).HashKey(): 1,
(&object.String{Value: "two"}).HashKey(): 2,
(&object.String{Value: "three"}).HashKey(): 3,
(&object.Integer{Value: 4}).HashKey(): 4,
TRUE.HashKey(): 5,
FALSE.HashKey(): 6,
}

if len(result.Pairs) != len(expected) {
t.Fatalf("hash has wrong number of pairs. got=%d", len(result.Pairs))
}

for expectedKey, expectedValue := range expected {
pair, ok := result.Pairs[expectedKey]
if !ok {
t.Errorf("no pair for given key in Pairs")
}

testIntegerObject(t, pair.Value, expectedValue)
}
}

func TestHashIndexExpressions(t *testing.T) {
tests := []struct {
input string
expected interface{}
}{
{`{"foo": 5}["foo"]`, 5},
{`{"foo": 5}["bar"]`, nil},
{`let key = "foo"; {"foo": 5}[key]`, 5},
{`{}["foo"]`, nil},
{`{5: 5}[5]`, 5},
{`{true: 5}[true]`, 5},
{`{false: 5}[false]`, 5},
}

for _, tt := range tests {
evaluated := testEval(tt.input)

switch expected := tt.expected.(type) {
case int:
testIntegerObject(t, evaluated, int64(expected))
case nil:
testNullObject(t, evaluated)
}
}
}

func testNullObject(t *testing.T, obj object.Object) bool {
if obj != NULL {
t.Errorf("object is not NULL. got=%T (%+v)", obj, obj)
Expand Down
63 changes: 61 additions & 2 deletions object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package object
import (
"bytes"
"fmt"
"hash/fnv"
"strings"

"github.com/vit0rr/mumu/ast"
Expand All @@ -16,10 +17,11 @@ const (
NULL_OBJ = "NULL"
RETURN_VALUE_OBJ = "RETURN_VALUE"
ERROR_OBJ = "ERROR"
FUNCTIONS_OBJ = "FUNCTIONS"
FUNCTION_OBJ = "FUNCTION"
STRING_OBJ = "STRING"
BUILTIN_OBJ = "BUILTIN"
ARRAY_OBJ = "ARRAY"
HASH_OBJ = "HASH"
)

type Object interface {
Expand Down Expand Up @@ -66,7 +68,7 @@ type Function struct {
Env *Environment
}

func (f *Function) Type() ObjectType { return FUNCTIONS_OBJ }
func (f *Function) Type() ObjectType { return FUNCTION_OBJ }
func (f *Function) Inspect() string {
var out bytes.Buffer

Expand Down Expand Up @@ -120,3 +122,60 @@ func (ao *Array) Inspect() string {

return out.String()
}

type HashKey struct {
Type ObjectType
Value uint64
}

func (b *Boolean) HashKey() HashKey {
var value uint64

if b.Value {
value = 1
} else {
value = 0
}

return HashKey{Type: b.Type(), Value: value}
}

func (i *Integer) HashKey() HashKey {
return HashKey{Type: i.Type(), Value: uint64(i.Value)}
}

func (s *String) HashKey() HashKey {
h := fnv.New64a()
h.Write([]byte(s.Value))

return HashKey{Type: s.Type(), Value: h.Sum64()}
}

type HashPair struct {
Key Object
Value Object
}

type Hash struct {
Pairs map[HashKey]HashPair
}

func (h *Hash) Type() ObjectType { return HASH_OBJ }
func (h *Hash) Inspect() string {
var out bytes.Buffer

pairs := []string{}
for _, pair := range h.Pairs {
pairs = append(pairs, fmt.Sprintf("%s: %s", pair.Key.Inspect(), pair.Value.Inspect()))
}

out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")

return out.String()
}

type Hashable interface {
HashKey() HashKey
}
22 changes: 22 additions & 0 deletions object/object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package object

import "testing"

func TestStringHashKey(t *testing.T) {
hello1 := &String{Value: "Hello World"}
hello2 := &String{Value: "Hello World"}
diff1 := &String{Value: "My name is vitor"}
diff2 := &String{Value: "My name is vitor"}

if hello1.HashKey() != hello2.HashKey() {
t.Errorf("strings with same content have different hash keys")
}

if diff1.HashKey() != diff2.HashKey() {
t.Errorf("strings with same content have different hash keys")
}

if hello1.HashKey() == diff1.HashKey() {
t.Errorf("strings with different content have same hash keys")
}
}

0 comments on commit 4575770

Please sign in to comment.