Skip to content

Commit

Permalink
Support hash literal
Browse files Browse the repository at this point in the history
  • Loading branch information
kitasuke committed Nov 11, 2018
1 parent c46d76d commit e1716fd
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 0 deletions.
22 changes: 22 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
46 changes: 46 additions & 0 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
Expand All @@ -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
Expand All @@ -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:
Expand Down
89 changes: 89 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) {
`, 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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ')':
Expand Down
6 changes: 6 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestNextToken(t *testing.T) {
"foobar"
"foo bar"
[1, 2];
{"foo": "bar"}
`

tests := []struct {
Expand Down Expand Up @@ -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, ""},
}

Expand Down
57 changes: 57 additions & 0 deletions object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -33,13 +45,27 @@ 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
}

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{}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
}
16 changes: 16 additions & 0 deletions object/object_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
30 changes: 30 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit e1716fd

Please # to comment.