Skip to content

Commit

Permalink
Add support for hash maps
Browse files Browse the repository at this point in the history
  • Loading branch information
claudemuller committed Jun 14, 2023
1 parent e790e47 commit 1f2d2df
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 268 deletions.
23 changes: 23 additions & 0 deletions internal/pkg/ast/ast.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ast

import (
"bytes"
"strings"

"github.com/claudemuller/oohooh-aahaah-go/internal/pkg/token"
Expand Down Expand Up @@ -347,3 +348,25 @@ func (ie *IndexExpression) String() string {

return out.String()
}

type HashLiteral struct {
Token token.Token // the '{' token
Pairs map[Expression]Expression
}

func (hl *HashLiteral) expressionNode() {}
func (hl *HashLiteral) TokenLiteral() string {
return hl.Token.Literal
}
func (hl *HashLiteral) String() string {
var out bytes.Buffer
var pairs []string

for key, value := range hl.Pairs {
pairs = append(pairs, key.String()+":"+value.String())
}
out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")
return out.String()
}
47 changes: 47 additions & 0 deletions internal/pkg/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
}

return evalIndexExpression(left, index)

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

return nil
Expand Down Expand Up @@ -338,6 +341,8 @@ 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())
}
Expand All @@ -355,6 +360,48 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {
return arrayObj.Elements[idx]
}

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 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 newError(format string, a ...interface{}) *object.Error {
return &object.Error{Message: fmt.Sprintf(format, a...)}
}
Expand Down
179 changes: 90 additions & 89 deletions internal/pkg/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,10 @@ if (10 > 1) {
"foobar",
"identifier not found: foobar",
},
// {
// `{"name": "Monkey"}[fn(x) { x }];`,
// "unusable as hash key: FUNCTION",
// },
{
`{"name": "Monkey"}[fn(x) { x }];`,
"unusable as hash key: FUNCTION",
},
{
`999[1]`,
"index operator not supported: INTEGER",
Expand Down Expand Up @@ -502,91 +502,92 @@ 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("Eval didn't return 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 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")
// }
//
// 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)
// integer, ok := tt.expected.(int)
// if ok {
// testIntegerObject(t, evaluated, int64(integer))
// } else {
// testNullObject(t, evaluated)
// }
// }
// }
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("Eval didn't return 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 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")
}

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)
integer, ok := tt.expected.(int)
if ok {
testIntegerObject(t, evaluated, int64(integer))
} else {
testNullObject(t, evaluated)
}
}
}

func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ func (l *Lexer) NextToken() token.Token {
tok = newToken(token.GT, l.ch)
case ';':
tok = newToken(token.SEMICOLON, l.ch)
case ':':
tok = newToken(token.COLON, l.ch)
case ',':
tok = newToken(token.COMMA, l.ch)
case '(':
Expand Down
12 changes: 6 additions & 6 deletions internal/pkg/lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ if (5 < 10) {
"foobar"
"foo bar"
[1, 2];
{"foo": "bar"}
`
// {"foo": "bar"}

tests := []struct {
expectedType token.TokenType
Expand Down Expand Up @@ -117,11 +117,11 @@ if (5 < 10) {
{token.INT, "2"},
{token.RBRACKET, "]"},
{token.SEMICOLON, ";"},
// {token.LBRACE, "{"},
// {token.STRING, "foo"},
// {token.COLON, ":"},
// {token.STRING, "bar"},
// {token.RBRACE, "}"},
{token.LBRACE, "{"},
{token.STRING, "foo"},
{token.COLON, ":"},
{token.STRING, "bar"},
{token.RBRACE, "}"},
{token.EOF, ""},
}

Expand Down
Loading

0 comments on commit 1f2d2df

Please sign in to comment.