Skip to content

Commit

Permalink
Add map type (#6)
Browse files Browse the repository at this point in the history
* remove pull_request and master push build

* init

* Add map parser

* init map evaluator

* will merge

* add hashmap examples

* Update README.md
  • Loading branch information
sevenreup authored May 15, 2024
1 parent 465400b commit 5b8bd9c
Show file tree
Hide file tree
Showing 18 changed files with 644 additions and 9 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ name: build

on:
push:
branches:
- "mbuye"
tags:
- "v*"
pull_request:

jobs:
goreleaser:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ You can check out examples in `./examples` folder to quickly see what the langua
- [ ] Type checking (for now it does not strictly enforce types)
- [ ] Data Structures
- [x] arrays
- [ ] dictionaries
- [x] dictionaries
- [ ] Input/ Output
- [ ] Error Handling
- [ ] Class support
Expand Down
7 changes: 7 additions & 0 deletions examples/map.ny
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mgwirizano munthu = {
"dzina": "Soma",
"zaka": 3
}

lembanzr(munthu["dzina"])
lembanzr(munthu["zaka"])
18 changes: 18 additions & 0 deletions src/ast/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ast

import (
"github.com/sevenreup/chewa/src/token"
)

type MapExpression struct {
Expression
Token token.Token
Pairs map[Expression]Expression
}

func (ie *MapExpression) TokenLiteral() string { return ie.Token.Literal }

// TODO: Print this one properly
func (ie *MapExpression) String() string {
return ""
}
2 changes: 2 additions & 0 deletions src/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
// Expressions
case *ast.IntegerLiteral:
return &object.Integer{Value: node.Value}
case *ast.MapExpression:
return evalMapExpression(node, env)
case *ast.Boolean:
return nativeBoolToBooleanObject(node.Value)
case *ast.StringLiteral:
Expand Down
84 changes: 84 additions & 0 deletions src/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ func TestErrorHandling(t *testing.T) {
`"Hello"- "World"`,
"unknown operator: STRING - STRING",
},
{
`{"dzina": "Maliko"}[ndondomeko d(x) { x }];`,
"1:20:: runtime error: unusable as map key: FUNCTION",
},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
Expand Down Expand Up @@ -593,3 +597,83 @@ func TestMethodCalls(t *testing.T) {
}
}
}

func TestHashLiterals(t *testing.T) {
input := `mawu two = "two";
{
"one": 10- 9,
two: 1 + 1,
"thr" + "ee": 6 / 2,
4: 4,
zoona: 5,
bodza: 6
}`
evaluated := testEval(input)
result, ok := evaluated.(*object.Map)
if !ok {
t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated)
}
expected := map[object.MapKey]int64{
(&object.String{Value: "one"}).MapKey(): 1,
(&object.String{Value: "two"}).MapKey(): 2,
(&object.String{Value: "three"}).MapKey(): 3,
(&object.Integer{Value: decimal.NewFromInt(4)}).MapKey(): 4,
TRUE.MapKey(): 5,
FALSE.MapKey(): 6,
}
if len(result.Pairs) != len(expected) {
t.Fatalf("Hash has wrong num 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")
}
testLiteralExpression(t, pair.Value, expectedValue)
}
}

func TestHashIndexExpressions(t *testing.T) {
tests := []struct {
input string
expected interface{}
}{
{
`{"foo": 5}["foo"]`,
5,
},
{
`{"foo": 5}["bar"]`,
nil,
},
{
`mawu key = "foo"; {"foo": 5}[key]`,
5,
},
{
`{}["foo"]`,
nil,
},
{
`{5: 5}[5]`,
5,
},
{
`{zoona: 5}[zoona]`,
5,
},
{
`{bodza: 5}[bodza]`,
5,
},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
integer, ok := tt.expected.(int)
if ok {
testLiteralExpression(t, evaluated, int64(integer))
} else {
testNullObject(t, evaluated)
}
}
}
20 changes: 20 additions & 0 deletions src/evaluator/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func evalIndexExpression(node *ast.IndexExpression, env *object.Environment) obj
switch {
case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
return evalArrayIndexExpression(left, index)
case left.Type() == object.MAP_OBJ:
return evaluateMapIndex(node, left, index)
default:
return newError("index operator not supported: %s", left.Type())
}
Expand All @@ -34,3 +36,21 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {

return arrayObject.Elements[idx]
}

func evaluateMapIndex(node *ast.IndexExpression, left, index object.Object) object.Object {
mapObject := left.(*object.Map)

key, ok := index.(object.Mappable)

if !ok {
return newError("%d:%d:%s: runtime error: unusable as map key: %s", node.Token.Pos.Line, node.Token.Pos.Column, node.Token.File, index.Type())
}

pair, ok := mapObject.Pairs[key.MapKey()]

if !ok {
return NULL
}

return pair.Value
}
46 changes: 46 additions & 0 deletions src/evaluator/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package evaluator

import (
"github.com/sevenreup/chewa/src/ast"
"github.com/sevenreup/chewa/src/object"
)

func evalMapExpression(node *ast.MapExpression,
env *object.Environment) object.Object {
pairs := make(map[object.MapKey]object.MapPair)

for keyNode, valueNode := range node.Pairs {
identifier, ok := keyNode.(*ast.Identifier)

if ok {
keyNode = &ast.StringLiteral{
Token: identifier.Token,
Value: identifier.Value,
}
}

key := Eval(keyNode, env)

if isError(key) {
return key
}

mapKey, ok := key.(object.Mappable)

if !ok {
return newError("%d:%d:%s: runtime error: unusable as map key: %s", node.Token.Pos.Line, node.Token.Pos.Column, node.Token.File, key.Type())
}

value := Eval(valueNode, env)

if isError(value) {
return value
}

hashed := mapKey.MapKey()

pairs[hashed] = object.MapPair{Key: key, Value: value}
}

return &object.Map{Pairs: pairs}
}
12 changes: 11 additions & 1 deletion src/object/boolean.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package object

import "fmt"
import (
"fmt"
"hash/fnv"
)

const BOOLEAN_OBJ = "BOOLEAN"

type Boolean struct {
Object
Mappable
Value bool
}

Expand All @@ -17,3 +21,9 @@ func (i *Boolean) Method(method string, args []Object) (Object, bool) {
//TODO implement me
panic("implement me")
}

func (s *Boolean) MapKey() MapKey {
h := fnv.New64a()
h.Write([]byte(fmt.Sprint(s.Value)))
return MapKey{Type: s.Type(), Value: h.Sum64()}
}
11 changes: 11 additions & 0 deletions src/object/integer.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package object

import (
"fmt"
"hash/fnv"

"github.com/shopspring/decimal"
)

const INTEGER_OBJ = "INTEGER"

type Integer struct {
Object
Mappable
Value decimal.Decimal
}

Expand All @@ -18,3 +23,9 @@ func (i *Integer) Method(method string, args []Object) (Object, bool) {
//TODO implement me
panic("implement me")
}

func (s *Integer) MapKey() MapKey {
h := fnv.New64a()
h.Write([]byte(fmt.Sprint(s.Value)))
return MapKey{Type: s.Type(), Value: h.Sum64()}
}
38 changes: 38 additions & 0 deletions src/object/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package object

import (
"bytes"
"fmt"
"strings"
)

const MAP_OBJ = "MAP"

type Map struct {
Pairs map[MapKey]MapPair
}

type MapPair struct {
Key Object
Value Object
}

func (m *Map) Type() ObjectType { return MAP_OBJ }

func (m *Map) Inspect() string {
var out bytes.Buffer
pairs := []string{}
for _, pair := range m.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()
}

func (m *Map) Method(method string, args []Object) (Object, bool) {
//TODO implement me
panic("implement me")
}
9 changes: 9 additions & 0 deletions src/object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ type Object interface {
Inspect() string
}

type MapKey struct {
Type ObjectType
Value uint64
}

type Mappable interface {
MapKey() MapKey
}

type HasMethods interface {
Method(method string, args []Object) (Object, bool)
}
Expand Down
19 changes: 19 additions & 0 deletions src/object/object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package object

import "testing"

func TestStringMapKey(t *testing.T) {
hello1 := &String{Value: "Hello World"}
hello2 := &String{Value: "Hello World"}
diff1 := &String{Value: "My name is johnny"}
diff2 := &String{Value: "My name is johnny"}
if hello1.MapKey() != hello2.MapKey() {
t.Errorf("strings with same content have different hash keys")
}
if diff1.MapKey() != diff2.MapKey() {
t.Errorf("strings with same content have different hash keys")
}
if hello1.MapKey() == diff1.MapKey() {
t.Errorf("strings with different content have same hash keys")
}
}
10 changes: 10 additions & 0 deletions src/object/string.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package object

import "hash/fnv"

const STRING_OBJ = "STRING"

type String struct {
Object
Mappable
Value string
}

Expand All @@ -14,3 +18,9 @@ func (i *String) Method(method string, args []Object) (Object, bool) {
//TODO implement me
panic("implement me")
}

func (s *String) MapKey() MapKey {
h := fnv.New64a()
h.Write([]byte(s.Value))
return MapKey{Type: s.Type(), Value: h.Sum64()}
}
Loading

0 comments on commit 5b8bd9c

Please sign in to comment.