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() {}
|
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 index
|
||||||
}
|
}
|
||||||
return evalIndexExpression(left, index)
|
return evalIndexExpression(left, index)
|
||||||
|
|
||||||
|
case *ast.HashLiteral:
|
||||||
|
return evalHashLiteral(node, env)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -345,11 +349,29 @@ func evalIndexExpression(left, index object.Object) object.Object {
|
|||||||
switch {
|
switch {
|
||||||
case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
|
case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
|
||||||
return evalArrayIndexExpression(left, index)
|
return evalArrayIndexExpression(left, index)
|
||||||
|
case left.Type() == object.HASH_OBJ:
|
||||||
|
return evalHashIndexExpression(left, index)
|
||||||
default:
|
default:
|
||||||
return newError("index operator not supported: %s", left.Type())
|
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 {
|
func evalArrayIndexExpression(array, index object.Object) object.Object {
|
||||||
arrayObject := array.(*object.Array)
|
arrayObject := array.(*object.Array)
|
||||||
idx := index.(*object.Integer).Value
|
idx := index.(*object.Integer).Value
|
||||||
@@ -361,3 +383,32 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {
|
|||||||
|
|
||||||
return arrayObject.Elements[idx]
|
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"`,
|
`"Hello" - "World"`,
|
||||||
"unknown operator: STRING - STRING",
|
"unknown operator: STRING - STRING",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
`{"name": "Monkey"}[fn(x) { x }];`,
|
||||||
|
"unusable as hash key: FUNCTION",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
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 {
|
func testEval(input string) object.Object {
|
||||||
l := lexer.New(input)
|
l := lexer.New(input)
|
||||||
p := parser.New(l)
|
p := parser.New(l)
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ func (l *Lexer) NextToken() token.Token {
|
|||||||
tok = newToken(token.LBRACKET, l.ch)
|
tok = newToken(token.LBRACKET, l.ch)
|
||||||
case ']':
|
case ']':
|
||||||
tok = newToken(token.RBRACKET, l.ch)
|
tok = newToken(token.RBRACKET, l.ch)
|
||||||
|
case ':':
|
||||||
|
tok = newToken(token.COLON, l.ch)
|
||||||
default:
|
default:
|
||||||
if isLetter(l.ch) {
|
if isLetter(l.ch) {
|
||||||
tok.Literal = l.readIdentifier()
|
tok.Literal = l.readIdentifier()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func TestNextToken(t *testing.T) {
|
|||||||
"foobar"
|
"foobar"
|
||||||
"foo bar"
|
"foo bar"
|
||||||
[1, 2];
|
[1, 2];
|
||||||
|
{"foo": "bar"}
|
||||||
`
|
`
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -126,6 +127,12 @@ func TestNextToken(t *testing.T) {
|
|||||||
{token.RBRACKET, "]"},
|
{token.RBRACKET, "]"},
|
||||||
{token.SEMICOLON, ";"},
|
{token.SEMICOLON, ";"},
|
||||||
|
|
||||||
|
{token.LBRACE, "{"},
|
||||||
|
{token.STRING, "foo"},
|
||||||
|
{token.COLON, ":"},
|
||||||
|
{token.STRING, "bar"},
|
||||||
|
{token.RBRACE, "}"},
|
||||||
|
|
||||||
{token.EOF, ""},
|
{token.EOF, ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package object
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
"monkey/ast"
|
"monkey/ast"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -19,6 +20,7 @@ const (
|
|||||||
STRING_OBJ = "STRING"
|
STRING_OBJ = "STRING"
|
||||||
BUILTIN_OBJ = "BUILTIN"
|
BUILTIN_OBJ = "BUILTIN"
|
||||||
ARRAY_OBJ = "ARRAY"
|
ARRAY_OBJ = "ARRAY"
|
||||||
|
HASH_OBJ = "HASH"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Object interface {
|
type Object interface {
|
||||||
@@ -157,3 +159,63 @@ func (ao Array) Inspect() string {
|
|||||||
|
|
||||||
return out.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.FUNCTION, p.parseFunctionLiteral)
|
||||||
p.registerPrefix(token.STRING, p.parseStringLiteral)
|
p.registerPrefix(token.STRING, p.parseStringLiteral)
|
||||||
p.registerPrefix(token.LBRACKET, p.parseArrayLiteral)
|
p.registerPrefix(token.LBRACKET, p.parseArrayLiteral)
|
||||||
|
p.registerPrefix(token.LBRACE, p.parseHashLiteral)
|
||||||
|
|
||||||
p.infixParseFns = make(map[token.TokenType]infixParseFn)
|
p.infixParseFns = make(map[token.TokenType]infixParseFn)
|
||||||
p.registerInfix(token.PLUS, p.parseInfixExpression)
|
p.registerInfix(token.PLUS, p.parseInfixExpression)
|
||||||
@@ -458,3 +459,32 @@ func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression {
|
|||||||
|
|
||||||
return exp
|
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 {
|
func testLetStatement(t *testing.T, s ast.Statement, name string) bool {
|
||||||
if s.TokenLiteral() != "let" {
|
if s.TokenLiteral() != "let" {
|
||||||
t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral())
|
t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral())
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const (
|
|||||||
// Delimiters
|
// Delimiters
|
||||||
COMMA = ","
|
COMMA = ","
|
||||||
SEMICOLON = ";"
|
SEMICOLON = ";"
|
||||||
|
COLON = ":"
|
||||||
|
|
||||||
LPAREN = "("
|
LPAREN = "("
|
||||||
RPAREN = ")"
|
RPAREN = ")"
|
||||||
|
|||||||
Reference in New Issue
Block a user