Hashes
This commit is contained in:
26
ast/ast.go
26
ast/ast.go
@@ -325,3 +325,29 @@ func (ie IndexExpression) String() string {
|
||||
}
|
||||
|
||||
func (ie IndexExpression) expressionNode() {}
|
||||
|
||||
type HashLiteral struct {
|
||||
Token token.Token // the '{' token
|
||||
Pairs map[Expression]Expression
|
||||
}
|
||||
|
||||
func (hl HashLiteral) TokenLiteral() string {
|
||||
return hl.Token.Literal
|
||||
}
|
||||
|
||||
func (hl HashLiteral) String() string {
|
||||
var out bytes.Buffer
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (hl HashLiteral) expressionNode() {}
|
||||
|
||||
@@ -114,6 +114,10 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
|
||||
return index
|
||||
}
|
||||
return evalIndexExpression(left, index)
|
||||
|
||||
case *ast.HashLiteral:
|
||||
return evalHashLiteral(node, env)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -345,11 +349,29 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
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 evalArrayIndexExpression(array, index object.Object) object.Object {
|
||||
arrayObject := array.(*object.Array)
|
||||
idx := index.(*object.Integer).Value
|
||||
@@ -361,3 +383,32 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {
|
||||
|
||||
return arrayObject.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}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,10 @@ func TestErrorHandling(t *testing.T) {
|
||||
`"Hello" - "World"`,
|
||||
"unknown operator: STRING - STRING",
|
||||
},
|
||||
{
|
||||
`{"name": "Monkey"}[fn(x) { x }];`,
|
||||
"unusable as hash key: FUNCTION",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -434,6 +438,93 @@ 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 pair. 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)
|
||||
|
||||
@@ -98,6 +98,8 @@ func (l *Lexer) NextToken() token.Token {
|
||||
tok = newToken(token.LBRACKET, l.ch)
|
||||
case ']':
|
||||
tok = newToken(token.RBRACKET, l.ch)
|
||||
case ':':
|
||||
tok = newToken(token.COLON, l.ch)
|
||||
default:
|
||||
if isLetter(l.ch) {
|
||||
tok.Literal = l.readIdentifier()
|
||||
|
||||
@@ -28,6 +28,7 @@ func TestNextToken(t *testing.T) {
|
||||
"foobar"
|
||||
"foo bar"
|
||||
[1, 2];
|
||||
{"foo": "bar"}
|
||||
`
|
||||
|
||||
tests := []struct {
|
||||
@@ -126,6 +127,12 @@ func TestNextToken(t *testing.T) {
|
||||
{token.RBRACKET, "]"},
|
||||
{token.SEMICOLON, ";"},
|
||||
|
||||
{token.LBRACE, "{"},
|
||||
{token.STRING, "foo"},
|
||||
{token.COLON, ":"},
|
||||
{token.STRING, "bar"},
|
||||
{token.RBRACE, "}"},
|
||||
|
||||
{token.EOF, ""},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package object
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"monkey/ast"
|
||||
"strings"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ const (
|
||||
STRING_OBJ = "STRING"
|
||||
BUILTIN_OBJ = "BUILTIN"
|
||||
ARRAY_OBJ = "ARRAY"
|
||||
HASH_OBJ = "HASH"
|
||||
)
|
||||
|
||||
type Object interface {
|
||||
@@ -157,3 +159,63 @@ func (ao Array) Inspect() string {
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
type HashKey struct {
|
||||
Type ObjectType
|
||||
Value uint64
|
||||
}
|
||||
|
||||
func (b *Boolean) HashKey() HashKey {
|
||||
var value uint64
|
||||
|
||||
if b.Value {
|
||||
value = 1
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
|
||||
return HashKey{Type: b.Type(), Value: value}
|
||||
}
|
||||
|
||||
type Hashable interface {
|
||||
HashKey() HashKey
|
||||
}
|
||||
|
||||
func (i *Integer) HashKey() HashKey {
|
||||
return HashKey{Type: i.Type(), Value: uint64(i.Value)}
|
||||
}
|
||||
|
||||
func (s *String) HashKey() HashKey {
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(s.Value))
|
||||
|
||||
return HashKey{Type: s.Type(), Value: h.Sum64()}
|
||||
}
|
||||
|
||||
type HashPair struct {
|
||||
Key Object
|
||||
Value Object
|
||||
}
|
||||
|
||||
type Hash struct {
|
||||
Pairs map[HashKey]HashPair
|
||||
}
|
||||
|
||||
func (h *Hash) Type() ObjectType {
|
||||
return HASH_OBJ
|
||||
}
|
||||
|
||||
func (h *Hash) Inspect() string {
|
||||
var out bytes.Buffer
|
||||
|
||||
pairs := []string{}
|
||||
for _, pair := range h.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()
|
||||
}
|
||||
|
||||
22
object/object_test.go
Normal file
22
object/object_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
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() {
|
||||
t.Errorf("string with same content have different hash keys")
|
||||
}
|
||||
|
||||
if diff1.HashKey() != diff2.HashKey() {
|
||||
t.Errorf("string with same content have different hash keys")
|
||||
}
|
||||
|
||||
if hello1.HashKey() == diff1.HashKey() {
|
||||
t.Errorf("string with different content have same hash keys")
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ func New(l *lexer.Lexer) *Parser {
|
||||
p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral)
|
||||
p.registerPrefix(token.STRING, p.parseStringLiteral)
|
||||
p.registerPrefix(token.LBRACKET, p.parseArrayLiteral)
|
||||
p.registerPrefix(token.LBRACE, p.parseHashLiteral)
|
||||
|
||||
p.infixParseFns = make(map[token.TokenType]infixParseFn)
|
||||
p.registerInfix(token.PLUS, p.parseInfixExpression)
|
||||
@@ -458,3 +459,32 @@ func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression {
|
||||
|
||||
return exp
|
||||
}
|
||||
|
||||
func (p *Parser) parseHashLiteral() ast.Expression {
|
||||
hash := &ast.HashLiteral{Token: p.curToken}
|
||||
hash.Pairs = make(map[ast.Expression]ast.Expression)
|
||||
|
||||
for !p.peekTokenIs(token.RBRACE) {
|
||||
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.RBRACE) && !p.expectPeek(token.COMMA) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !p.expectPeek(token.RBRACE) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
@@ -743,6 +743,108 @@ func TestParsingIndexExpressions(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 not *ast.HashLiteral. got=%T", stmt.Expression)
|
||||
}
|
||||
|
||||
if len(hash.Pairs) != 3 {
|
||||
t.Fatalf("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 ast.StringLiteral. got=%T", 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 not *ast.HashLiteral. got=%T", 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 not *ast.HashLiteral. got=%T", stmt.Expression)
|
||||
}
|
||||
|
||||
if len(hash.Pairs) != 3 {
|
||||
t.Fatalf("hash.Pairs has wrong length. got=%d", len(hash.Pairs))
|
||||
}
|
||||
|
||||
tests := map[string]func(expression 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 ast.StringLiteral. got=%T", key)
|
||||
continue
|
||||
}
|
||||
|
||||
testFunc, ok := tests[literal.String()]
|
||||
if !ok {
|
||||
t.Errorf("No test function for key %q found", literal.String())
|
||||
continue
|
||||
}
|
||||
|
||||
testFunc(value)
|
||||
}
|
||||
}
|
||||
|
||||
func testLetStatement(t *testing.T, s ast.Statement, name string) bool {
|
||||
if s.TokenLiteral() != "let" {
|
||||
t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral())
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
// Delimiters
|
||||
COMMA = ","
|
||||
SEMICOLON = ";"
|
||||
COLON = ":"
|
||||
|
||||
LPAREN = "("
|
||||
RPAREN = ")"
|
||||
|
||||
Reference in New Issue
Block a user