umx_compiler

UMX virtual machine "Monkey" interpreter / bytecode compiler
git clone git://bsandro.tech/umx_compiler
Log | Files | Refs

commit 0632ef9289a8bbcbb98af5c0f1f3c0997eecf040
parent 6c59b6321eacbed957c968a8509a8ada02b6db2c
Author: bsandro <email@bsandro.tech>
Date:   Thu, 30 Jun 2022 23:58:23 +0300

hashes support

Diffstat:
Mast/ast.go | 21+++++++++++++++++++++
Meval/eval.go | 38++++++++++++++++++++++++++++++++++++++
Meval/eval_test.go | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlexer/lexer.go | 2++
Mlexer/lexer_test.go | 9+++++++++
Mobject/object.go | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Aobject/object_test.go | 21+++++++++++++++++++++
Mparser/parser.go | 24++++++++++++++++++++++++
Mparser/parser_test.go | 29+++++++++++++++++++++++++++++
Mtoken/token.go | 1+
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{