Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add map type #6

Merged
merged 7 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading