commit 0632ef9289a8bbcbb98af5c0f1f3c0997eecf040
parent 6c59b6321eacbed957c968a8509a8ada02b6db2c
Author: bsandro <email@bsandro.tech>
Date: Thu, 30 Jun 2022 23:58:23 +0300
hashes support
Diffstat:
10 files changed, 256 insertions(+), 0 deletions(-)
diff --git a/ast/ast.go b/ast/ast.go
@@ -319,3 +319,24 @@ func (ie *IndexExpression) String() string {
out.WriteString("])")
return out.String()
}
+
+type HashLiteral struct {
+ Token token.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
+ pairs := []string{}
+ for k, v := range hl.Pairs {
+ pairs = append(pairs, k.String()+":"+v.String())
+ }
+ out.WriteString("{")
+ out.WriteString(strings.Join(pairs, ", "))
+ out.WriteString("}")
+ return out.String()
+}
diff --git a/eval/eval.go b/eval/eval.go
@@ -107,6 +107,8 @@ func Eval(node ast.Node, ctx *object.Context) object.Object {
return index
}
return evalIndexExpression(left, index)
+ case *ast.HashLiteral:
+ return evalHashLiteral(node, ctx)
}
return nil
}
@@ -317,6 +319,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())
}
@@ -331,3 +335,37 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {
}
return arrayObject.Elements[idx]
}
+
+func evalHashLiteral(node *ast.HashLiteral, ctx *object.Context) object.Object {
+ pairs := make(map[object.HashKey]object.HashPair)
+ for knode, vnode := range node.Pairs {
+ k := Eval(knode, ctx)
+ if isError(k) {
+ return k
+ }
+ hashKey, ok := k.(object.Hashable)
+ if !ok {
+ return newError("unusable as hash key: %s", k.Type())
+ }
+ v := Eval(vnode, ctx)
+ if isError(v) {
+ return v
+ }
+ hashed := hashKey.HashKey()
+ pairs[hashed] = object.HashPair{Key: k, Value: v}
+ }
+ return &object.Hash{Pairs: pairs}
+}
+
+func evalHashIndexExpression(hash, index object.Object) object.Object {
+ hashObj := hash.(*object.Hash)
+ key, ok := index.(object.Hashable)
+ if !ok {
+ return newError("unusable as hash key: %s", index.Type())
+ }
+ pair, ok := hashObj.Pairs[key.HashKey()]
+ if !ok {
+ return NULL
+ }
+ return pair.Value
+}
diff --git a/eval/eval_test.go b/eval/eval_test.go
@@ -165,6 +165,7 @@ func TestErrorHandling(t *testing.T) {
{"if (10>1) { true + false; }", "unknown operator: BOOLEAN + BOOLEAN"},
{"foo", "identifier not found: foo"},
{`"foo"-"bar"`, "unknown operator: STRING - STRING"},
+ {`{"foo": "bar"}[fn(x){x}]`, "unusable as hash key: FUNCTION"},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
@@ -324,3 +325,63 @@ func TestArrayIndexExpression(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("no hash returned")
+ }
+ 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("invalid number of pairs in the hash")
+ }
+ for k, v := range expected {
+ pair, ok := result.Pairs[k]
+ if !ok {
+ t.Fatalf("no pair for a given key")
+ }
+ testIntegerObject(t, pair.Value, v)
+ }
+}
+
+func TestHashEvalExpressions(t *testing.T) {
+ tests := []struct {
+ input string
+ expected interface{}
+ }{
+ {`{"foo":5}["foo"]`, 5},
+ {`{"foo":5}["bar"]`, nil},
+ {`let k="foo";{"foo":5}[k]`, 5},
+ {`{}["foo"]`, nil},
+ {`{4:8}[4]`, 8},
+ {`{true: 10}[true]`, 10},
+ {`{false: 16}[false]`, 16},
+ }
+ for _, tt := range tests {
+ evaluated := testEval(tt.input)
+ integer, ok := tt.expected.(int)
+ if ok {
+ testIntegerObject(t, evaluated, int64(integer))
+ } else {
+ testNullObject(t, evaluated)
+ }
+ }
+}
diff --git a/lexer/lexer.go b/lexer/lexer.go
@@ -82,6 +82,8 @@ func (l *Lexer) NextToken() token.Token {
case '"':
tok.Type = token.STRING
tok.Literal = l.readString()
+ case ':':
+ tok = newToken(token.COLON, l.ch)
case 0:
tok.Literal = ""
tok.Type = token.EOF
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
@@ -27,6 +27,8 @@ if (5 < 10) {
"foo bar";
[1, 2];
+
+{"foo":"bar"};
`
tests := []struct {
@@ -122,6 +124,13 @@ if (5 < 10) {
{token.RBRACKET, "]"},
{token.SEMICOLON, ";"},
+ {token.LCURLY, "{"},
+ {token.STRING, "foo"},
+ {token.COLON, ":"},
+ {token.STRING, "bar"},
+ {token.RCURLY, "}"},
+ {token.SEMICOLON, ";"},
+
{token.EOF, ""},
}
diff --git a/object/object.go b/object/object.go
@@ -3,6 +3,7 @@ package object
import (
"bytes"
"fmt"
+ "hash/fnv"
"interp/ast"
"strings"
)
@@ -20,6 +21,7 @@ const (
STRING_OBJ = "STRING"
BUILTIN_OBJ = "BUILTIN"
ARRAY_OBJ = "ARRAY"
+ HASH_OBJ = "HASH"
)
type Object interface {
@@ -111,3 +113,51 @@ func (ao *Array) Inspect() string {
out.WriteString("]")
return out.String()
}
+
+type HashKey struct {
+ Type ObjectType
+ Value uint64
+}
+
+func (s *String) HashKey() HashKey {
+ h := fnv.New64a()
+ h.Write([]byte(s.Value))
+ return HashKey{Type: s.Type(), Value: h.Sum64()}
+}
+func (i *Integer) HashKey() HashKey {
+ return HashKey{Type: i.Type(), Value: uint64(i.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 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()
+}
+
+type Hashable interface {
+ HashKey() HashKey
+}
diff --git a/object/object_test.go b/object/object_test.go
@@ -0,0 +1,21 @@
+package object
+
+import (
+ "testing"
+)
+
+func TestStringHashKey(t *testing.T) {
+ hello1 := &String{Value: "hello"}
+ hello2 := &String{Value: "hello"}
+ diff1 := &String{Value: "world"}
+ diff2 := &Boolean{Value: false}
+ if hello1.HashKey() != hello2.HashKey() {
+ t.Errorf("equal strings have different hashes")
+ }
+ if diff1.HashKey() == hello1.HashKey() {
+ t.Errorf("different strings have same hashes")
+ }
+ if diff1.HashKey() == diff2.HashKey() {
+ t.Errorf("different objects/types have same hashes")
+ }
+}
diff --git a/parser/parser.go b/parser/parser.go
@@ -83,6 +83,8 @@ func New(l *lexer.Lexer) *Parser {
p.registerInfix(token.LBRACKET, p.parseIndexExpression)
+ p.registerPrefix(token.LCURLY, p.parseHashLiteral)
+
return p
}
@@ -393,3 +395,25 @@ func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression {
}
return expr
}
+
+func (p *Parser) parseHashLiteral() ast.Expression {
+ hash := &ast.HashLiteral{Token: p.curToken}
+ hash.Pairs = make(map[ast.Expression]ast.Expression)
+ for !p.peekTokenIs(token.RCURLY) {
+ 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.RCURLY) && !p.expectPeek(token.COMMA) {
+ return nil
+ }
+ }
+ if !p.expectPeek(token.RCURLY) {
+ return nil
+ }
+ return hash
+}
diff --git a/parser/parser_test.go b/parser/parser_test.go
@@ -627,3 +627,32 @@ func TestParsingIndexExpressions(t *testing.T) {
return
}
}
+
+func TestParsingHashLiteralsStringKeys(t *testing.T) {
+ input := `{"one": 1, "two": 2, "three": 3};`
+ l := lexer.New(input)
+ p := New(l)
+ prg := p.ParseProgram()
+ checkParserErrors(t, p)
+ statement := prg.Statements[0].(*ast.ExpressionStatement)
+ hash, ok := statement.Expression.(*ast.HashLiteral)
+ if !ok {
+ t.Fatalf("expression is not ast.HashLiteral but %T", statement.Expression)
+ }
+ if len(hash.Pairs) != 3 {
+ t.Fatalf("Wrong number of elements in hash")
+ }
+ expected := map[string]int64{
+ "one": 1,
+ "two": 2,
+ "three": 3,
+ }
+ for k, v := range hash.Pairs {
+ literal, ok := k.(*ast.StringLiteral)
+ if !ok {
+ t.Fatalf("key is not ast.StringLiteral")
+ }
+ expectedVal := expected[literal.String()]
+ testIntegerLiteral(t, v, expectedVal)
+ }
+}
diff --git a/token/token.go b/token/token.go
@@ -38,6 +38,7 @@ const (
EQUAL = "=="
NOT_EQUAL = "!="
STRING = "STRING"
+ COLON = ":"
)
var keywords = map[string]TokenType{