diff --git a/ast/ast.go b/ast/ast.go index 4567d33..06c0531 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -310,3 +310,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()+token.Colon+value.String()) + } + + out.WriteString(token.LeftBrace) + out.WriteString(strings.Join(pairs, token.Comma+" ")) + out.WriteString(token.RightBrace) + + return out.String() +} diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 4eee2fd..5a3b2a9 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -100,6 +100,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return elements[0] } return &object.Array{Elements: elements} + case *ast.HashLiteral: + return evalHashLiteral(node, env) } return nil @@ -259,6 +261,8 @@ func evalIndexExpression(left, index object.Object) object.Object { switch { case left.Type() == object.ArrayObj && index.Type() == object.IntegerObj: return evalArrayIndexExpression(left, index) + case left.Type() == object.HashObj: + return evalHashIndexExpression(left, index) default: return newError("index operator not supported: %s", left.Type()) } @@ -276,6 +280,22 @@ func evalArrayIndexExpression(array, index object.Object) object.Object { return arrayObject.Elements[idx] } +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 evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object { if val, ok := env.Get(node.Value); ok { return val @@ -288,6 +308,32 @@ func evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object return newError("%s: %s", identifierNotFoundError, node.Value) } +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 applyFunction(fn object.Object, args []object.Object) object.Object { switch fn := fn.(type) { case *object.Function: diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 8a1adb4..33bd043 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -159,6 +159,7 @@ func TestErrorHandling(t *testing.T) { `, fmt.Sprintf("%s: %s + %s", unknownOperatorError, object.BooleanObj, object.BooleanObj)}, {"foobar", fmt.Sprintf("%s: %s", identifierNotFoundError, "foobar")}, {`"Hello" - "World"`, fmt.Sprintf("%s: %s - %s", unknownOperatorError, object.StringObj, object.StringObj)}, + {`{"name": "Monky"}[fn(x) { x }];`, fmt.Sprintf("unusable as hash key: %s", object.FunctionObj)}, } for _, tt := range tests { @@ -401,6 +402,94 @@ 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 %T. got=%T (%+v)", object.Hash{}, 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("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) diff --git a/lexer/lexer.go b/lexer/lexer.go index 0e57684..a3eb78b 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -57,6 +57,8 @@ func (l *Lexer) NextToken() token.Token { tok = newToken(token.Comma, l.ch) case ';': tok = newToken(token.Semicolon, l.ch) + case ':': + tok = newToken(token.Colon, l.ch) case '(': tok = newToken(token.LeftParen, l.ch) case ')': diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 26423c4..943062c 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -29,6 +29,7 @@ func TestNextToken(t *testing.T) { "foobar" "foo bar" [1, 2]; + {"foo": "bar"} ` tests := []struct { @@ -116,6 +117,11 @@ func TestNextToken(t *testing.T) { {token.Int, "2"}, {token.RightBracket, "]"}, {token.Semicolon, ";"}, + {token.LeftBrace, "{"}, + {token.String, "foo"}, + {token.Colon, ":"}, + {token.String, "bar"}, + {token.RightBrace, "}"}, {token.EOF, ""}, } diff --git a/object/object.go b/object/object.go index d997024..a6fd047 100644 --- a/object/object.go +++ b/object/object.go @@ -3,9 +3,11 @@ package object import ( "bytes" "fmt" + "hash/fnv" "strings" "github.com/kitasuke/monkey-go/ast" + "github.com/kitasuke/monkey-go/token" ) type ObjectType string @@ -20,8 +22,18 @@ const ( StringObj = "String" BuiltinObj = "Builtin" ArrayObj = "Array" + HashObj = "Hash" ) +type HashKey struct { + Type ObjectType + Value uint64 +} + +type Hashable interface { + HashKey() HashKey +} + type Object interface { Type() ObjectType Inspect() string @@ -33,6 +45,9 @@ type Integer struct { func (i *Integer) Type() ObjectType { return IntegerObj } func (i *Integer) Inspect() string { return fmt.Sprintf("%d", i.Value) } +func (i *Integer) HashKey() HashKey { + return HashKey{Type: i.Type(), Value: uint64(i.Value)} +} type Boolean struct { Value bool @@ -40,6 +55,17 @@ type Boolean struct { func (b *Boolean) Type() ObjectType { return BooleanObj } func (b *Boolean) Inspect() string { return fmt.Sprintf("%t", b.Value) } +func (b *Boolean) HashKey() HashKey { + var value uint64 + + if b.Value { + value = 1 + } else { + value = 0 + } + + return HashKey{Type: b.Type(), Value: value} +} type Null struct{} @@ -91,6 +117,12 @@ type String struct { func (s *String) Type() ObjectType { return StringObj } func (s *String) Inspect() string { return s.Value } +func (s *String) HashKey() HashKey { + h := fnv.New64a() + h.Write([]byte(s.Value)) + + return HashKey{Type: s.Type(), Value: h.Sum64()} +} type BuiltinFunction func(args ...Object) Object @@ -120,3 +152,28 @@ func (ao *Array) Inspect() string { return out.String() } + +type HashPair struct { + Key Object + Value Object +} + +type Hash struct { + Pairs map[HashKey]HashPair +} + +func (h *Hash) Type() ObjectType { return HashObj } +func (h *Hash) Inspect() string { + var out bytes.Buffer + + var pairs []string + for _, pair := range h.Pairs { + pairs = append(pairs, fmt.Sprintf("%s: %s", pair.Key.Inspect(), pair.Value.Inspect())) + } + + out.WriteString(token.LeftBrace) + out.WriteString(strings.Join(pairs, token.Comma+" ")) + out.WriteString(token.RightBrace) + + return out.String() +} diff --git a/object/object_test.go b/object/object_test.go new file mode 100644 index 0000000..ce530b0 --- /dev/null +++ b/object/object_test.go @@ -0,0 +1,16 @@ +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 johnny"} + diff2 := &String{Value: "My name is johnny"} + + if hello1.HashKey() != hello2.HashKey() || + diff1.HashKey() != diff2.HashKey() || + hello1.HashKey() == diff1.HashKey() { + t.Errorf("strings with same content have different hash keys") + } +} diff --git a/parser/parser.go b/parser/parser.go index f2f182b..8825aab 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -65,6 +65,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.Function, p.parseFunctionLiteral) p.registerPrefix(token.String, p.parseStringLiteral) p.registerPrefix(token.LeftBracket, p.parseArrayLiteral) + p.registerPrefix(token.LeftBrace, p.parseHashLiteral) p.infixParseFns = make(map[token.TokenType]infixParseFn) p.registerInfix(token.Plus, p.parseInfixExpression) @@ -386,6 +387,35 @@ func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression { return exp } +func (p *Parser) parseHashLiteral() ast.Expression { + hash := &ast.HashLiteral{Token: p.currentToken} + hash.Pairs = make(map[ast.Expression]ast.Expression) + + for !p.peekTokenIs(token.RightBrace) { + p.nextToken() + key := p.parseExpression(Lowest) + + if !p.expectPeek(token.Colon) { + return nil + } + + p.nextToken() + value := p.parseExpression(Lowest) + + hash.Pairs[key] = value + + if !p.peekTokenIs(token.RightBrace) && !p.expectPeek(token.Comma) { + return nil + } + } + + if !p.expectPeek(token.RightBrace) { + return nil + } + + return hash +} + // Infix expressions func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { diff --git a/parser/parser_test.go b/parser/parser_test.go index 77e5ab8..9dbaff7 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -523,6 +523,108 @@ func TestStringLiteralExpression(t *testing.T) { } } +func TestParsingHashLiteralsStringKeys(t *testing.T) { + input := `{"one": 1, "two": 2, "three": 3}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not %T. got=%T", ast.HashLiteral{}, stmt.Expression) + } + + if len(hash.Pairs) != 3 { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + + expected := map[string]int64{ + "one": 1, + "two": 2, + "three": 3, + } + + for key, value := range hash.Pairs { + literal, ok := key.(*ast.StringLiteral) + if !ok { + t.Errorf("key is not %T. got=%T", ast.StringLiteral{}, key) + } + + expectedValue := expected[literal.String()] + + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestParsingEmptyHashLiteral(t *testing.T) { + input := `{}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not %T. got=%T", ast.HashLiteral{}, stmt.Expression) + } + + if len(hash.Pairs) != 0 { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } +} + +func TestParsingHashLiteralsWithExpressions(t *testing.T) { + input := `{"one": 0 + 1, "two": 10 - 8, "three": 15 / 5}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not %T. got=%T", ast.HashLiteral{}, stmt.Expression) + } + + if len(hash.Pairs) != 3 { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + + tests := map[string]func(ast.Expression){ + "one": func(e ast.Expression) { + testInfixExpression(t, e, 0, "+", 1) + }, + "two": func(e ast.Expression) { + testInfixExpression(t, e, 10, "-", 8) + }, + "three": func(e ast.Expression) { + testInfixExpression(t, e, 15, "/", 5) + }, + } + + for key, value := range hash.Pairs { + literal, ok := key.(*ast.StringLiteral) + if !ok { + t.Errorf("key is not %T. got=%T", ast.StringLiteral{}, key) + continue + } + + testFunc, ok := tests[literal.String()] + if !ok { + t.Errorf("No test function for key %q found", literal.String()) + continue + } + + testFunc(value) + } +} + func createParseProgram(input string, t *testing.T) *ast.Program { l := lexer.New(input) p := New(l) diff --git a/token/token.go b/token/token.go index d70234b..ad6c8f3 100644 --- a/token/token.go +++ b/token/token.go @@ -32,6 +32,7 @@ const ( // Delimiters Comma = "," Semicolon = ";" + Colon = ":" LeftParen = "(" RightParen = ")"