From a3f218e1a6435a8edc86e18a5f897d08072e6b4f Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Sat, 27 Apr 2024 20:43:09 +0200 Subject: [PATCH 1/7] remove pull_request and master push build --- .github/workflows/release.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b932490..31997d5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,11 +2,8 @@ name: build on: push: - branches: - - "mbuye" tags: - "v*" - pull_request: jobs: goreleaser: From c738f3279e4b574a232e71d33f4f2da678eb4fe3 Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Mon, 29 Apr 2024 01:11:05 +0200 Subject: [PATCH 2/7] init --- src/parser/parser_test.go | 37 +++++++++++++++++++++++++++++++++++++ src/token/token.go | 2 ++ 2 files changed, 39 insertions(+) diff --git a/src/parser/parser_test.go b/src/parser/parser_test.go index 83e8e62..41ae6e4 100644 --- a/src/parser/parser_test.go +++ b/src/parser/parser_test.go @@ -1031,3 +1031,40 @@ func TestWhileExpressions(t *testing.T) { t.Fatalf("body is not *ast.BlockStatement. got=%T", whileExp.Consequence) } } + +func TestMapExpressions(t *testing.T) { + tests := []struct { + input string + }{ + { + input: `mgwirizano grades = {"one": 1, "two": 2, "three": 3}`, + }, + { + input: `mgwirizano grades = {}`, + }, + } + + for _, tt := range tests { + l := lexer.New([]byte(tt.input)) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + stmt := program.Statements[0].(*ast.ExpressionStatement) + whileExp, ok := stmt.Expression.(*ast.WhileExpression) + if !ok { + t.Fatalf("exp not *ast.WhileExpression. got=%T", stmt.Expression) + } + if !testInfixExpression(t, whileExp.Condition, "x", "<", 10) { + return + } + if len(whileExp.Consequence.Statements) != 1 { + t.Fatalf("whileExp.Block.Statements does not contain 1 statements. got=%d\n", + len(whileExp.Consequence.Statements)) + } + + if whileExp.Consequence == nil { + t.Fatalf("body is not *ast.BlockStatement. got=%T", whileExp.Consequence) + } + } + +} diff --git a/src/token/token.go b/src/token/token.go index a821c13..b3dc42a 100644 --- a/src/token/token.go +++ b/src/token/token.go @@ -62,6 +62,7 @@ const ( RETURN = "RETURN" FOR = "FOR" WHILE = "WHILE" + MAP = "MAP" ) type Position struct { @@ -87,6 +88,7 @@ var keywords = map[string]TokenType{ "ndondomeko": FUNCTION, "za": FOR, "pamene": WHILE, + "mgwirizano": MAP, } var variableTypes = map[TokenType]TokenType{ From ac428bafb21e788084a2af08bf6824db3ce34f88 Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Mon, 29 Apr 2024 23:11:33 +0200 Subject: [PATCH 3/7] Add map parser --- src/ast/map.go | 18 +++ src/parser/map.go | 46 +++++++ src/parser/parser.go | 11 +- src/parser/parser_test.go | 265 +++++++++++++++++++++++++++++++++++--- src/token/token.go | 1 + 5 files changed, 322 insertions(+), 19 deletions(-) create mode 100644 src/ast/map.go create mode 100644 src/parser/map.go diff --git a/src/ast/map.go b/src/ast/map.go new file mode 100644 index 0000000..58c5d17 --- /dev/null +++ b/src/ast/map.go @@ -0,0 +1,18 @@ +package ast + +import ( + "github.com/sevenreup/chewa/src/token" +) + +type MapExpression struct { + Expression + Token token.Token + Pairs map[Expression]Expression +} + +func (ie *MapExpression) TokenLiteral() string { return ie.Token.Literal } + +// TODO: Print this one properly +func (ie *MapExpression) String() string { + return "" +} diff --git a/src/parser/map.go b/src/parser/map.go new file mode 100644 index 0000000..29c5243 --- /dev/null +++ b/src/parser/map.go @@ -0,0 +1,46 @@ +package parser + +import ( + "github.com/sevenreup/chewa/src/ast" + "github.com/sevenreup/chewa/src/token" +) + +func (parser *Parser) mapLiteral() ast.Expression { + mapLiteral := &ast.MapExpression{Token: parser.curToken} + mapLiteral.Pairs = make(map[ast.Expression]ast.Expression) + + for !parser.peekTokenIs(token.OPENING_BRACE) { + if parser.peekTokenIs(token.CLOSING_BRACE) { + break + } + parser.nextToken() + + + key := parser.parseExpression(LOWEST) + + if !parser.expectPeek(token.COLON) { + return nil + } + + parser.nextToken() + + value := parser.parseExpression(LOWEST) + + mapLiteral.Pairs[key] = value + + if !parser.peekTokenIs(token.CLOSING_BRACE) && !parser.peekTokenIs(token.COMMA) { + return nil + } + if parser.peekTokenIs(token.CLOSING_BRACE) { + break + } + + parser.nextToken() + } + + if !parser.expectPeek(token.CLOSING_BRACE) { + return nil + } + + return mapLiteral +} diff --git a/src/parser/parser.go b/src/parser/parser.go index 90452fc..1b5142a 100644 --- a/src/parser/parser.go +++ b/src/parser/parser.go @@ -27,7 +27,7 @@ const ( PRODUCT PREFIX CALL - INDEX // array[index] + INDEX // array[index] ) var precedences = map[token.TokenType]int{ @@ -73,18 +73,21 @@ func New(l *lexer.Lexer) *Parser { p := &Parser{l: l, errors: []string{}} p.prefixParseFns = make(map[token.TokenType]prefixParseFn) - p.registerPrefix(token.IDENT, p.parseIdentifier) p.registerPrefix(token.INT, p.parseIntegerLiteral) + p.registerPrefix(token.STR, p.parseStringLiteral) + + p.registerPrefix(token.IDENT, p.parseIdentifier) p.registerPrefix(token.BANG, p.parsePrefixExpression) p.registerPrefix(token.MINUS, p.parsePrefixExpression) p.registerPrefix(token.TRUE, p.parseBoolean) p.registerPrefix(token.FALSE, p.parseBoolean) - p.registerPrefix(token.OPENING_PAREN, p.parseGroupedExpression) p.registerPrefix(token.IF, p.parseIfExpression) p.registerPrefix(token.FOR, p.parseForExpression) p.registerPrefix(token.WHILE, p.parseWhileExpression) p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral) - p.registerPrefix(token.STR, p.parseStringLiteral) + + p.registerPrefix(token.OPENING_BRACE, p.mapLiteral) + p.registerPrefix(token.OPENING_PAREN, p.parseGroupedExpression) p.registerPrefix(token.OPENING_BRACKET, p.parseArrayLiteral) p.infixParseFns = make(map[token.TokenType]infixParseFn) diff --git a/src/parser/parser_test.go b/src/parser/parser_test.go index 41ae6e4..b2d767a 100644 --- a/src/parser/parser_test.go +++ b/src/parser/parser_test.go @@ -1032,15 +1032,48 @@ func TestWhileExpressions(t *testing.T) { } } -func TestMapExpressions(t *testing.T) { +func TestMapExpressionsWithStringKeys(t *testing.T) { tests := []struct { - input string + input string + identifier string + length int + values map[string]int64 }{ { - input: `mgwirizano grades = {"one": 1, "two": 2, "three": 3}`, + input: `mgwirizano grades = {"one": 1, "two": 2, "three": 3};`, + identifier: "grades", + length: 3, + values: map[string]int64{"one": 1, "two": 2, "three": 3}, + }, + { + input: `mgwirizano grades = {"one": 1, "two": 2, "three": 3}`, + identifier: "grades", + length: 3, + values: map[string]int64{"one": 1, "two": 2, "three": 3}, + }, + { + input: `mgwirizano grades = {};`, + identifier: "grades", + length: 0, + values: map[string]int64{}, + }, + { + input: `mgwirizano grades = {}`, + identifier: "grades", + length: 0, + values: map[string]int64{}, + }, + { + input: `{}`, + identifier: "", + length: 0, + values: map[string]int64{}, }, { - input: `mgwirizano grades = {}`, + input: `{"one": 1, "two": 2, "three": 3};`, + identifier: "", + length: 3, + values: map[string]int64{"one": 1, "two": 2, "three": 3}, }, } @@ -1049,22 +1082,224 @@ func TestMapExpressions(t *testing.T) { p := New(l) program := p.ParseProgram() checkParserErrors(t, p) - stmt := program.Statements[0].(*ast.ExpressionStatement) - whileExp, ok := stmt.Expression.(*ast.WhileExpression) - if !ok { - t.Fatalf("exp not *ast.WhileExpression. got=%T", stmt.Expression) + var mapLiteral *ast.MapExpression + switch stmt := program.Statements[0].(type) { + case *ast.VariableDeclarationStatement: + { + expression, ok := stmt.Value.(*ast.MapExpression) + if !ok { + t.Fatalf("exp not *ast.MapExpression. got=%T", stmt.Value) + } + mapLiteral = expression + if stmt.Identifier.Value != tt.identifier { + t.Fatalf("exp not %s. got=%s", tt.identifier, stmt.Identifier.Value) + } + } + case *ast.ExpressionStatement: + { + expression, ok := stmt.Expression.(*ast.MapExpression) + if !ok { + t.Fatalf("exp not *ast.MapExpression. got=%T", stmt.Expression) + } + mapLiteral = expression + } + default: + { + t.Fatalf("exp not *ast.MapExpression or *ast.VariableDeclarationStatement. got=%T", stmt) + return + } + } - if !testInfixExpression(t, whileExp.Condition, "x", "<", 10) { - return + + if len(mapLiteral.Pairs) != tt.length { + t.Fatalf("map.Pairs has wrong length. got=%d", len(mapLiteral.Pairs)) } - if len(whileExp.Consequence.Statements) != 1 { - t.Fatalf("whileExp.Block.Statements does not contain 1 statements. got=%d\n", - len(whileExp.Consequence.Statements)) + + for key, value := range mapLiteral.Pairs { + identifier, ok := key.(*ast.StringLiteral) + + if !ok { + t.Errorf("key is not ast.StringLiteral. got=%T", key) + } + + expectedValue := tt.values[identifier.Value] + + testIntegerLiteral(t, value, expectedValue) } + } +} + +func TestMapLiteralsWithStringKeys(t *testing.T) { + input := `{"one": 1, "two": 2, "three": 3}` + + l := lexer.New([]byte(input)) + p := New(l) + program := p.ParseProgram() - if whileExp.Consequence == nil { - t.Fatalf("body is not *ast.BlockStatement. got=%T", whileExp.Consequence) + checkParserErrors(t, p) + + statement, ok := program.Statements[0].(*ast.ExpressionStatement) + + if !ok { + t.Fatalf("program.Statements[0] is not ast.Expression. got=%T", program.Statements[0]) + } + + mapLiteral, ok := statement.Expression.(*ast.MapExpression) + + if !ok { + t.Fatalf("statement is not ast.Map. got=%T", statement.Expression) + } + + if len(mapLiteral.Pairs) != 3 { + t.Fatalf("map.Pairs has wrong length. got=%d", len(mapLiteral.Pairs)) + } + + expected := map[string]int64{ + "one": 1, + "two": 2, + "three": 3, + } + + for key, value := range mapLiteral.Pairs { + literal, ok := key.(*ast.StringLiteral) + + if !ok { + t.Errorf("key is not ast.String. got=%T", key) } + + expectedValue := expected[literal.Value] + + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestMapLiteralsWithBooleanKeys(t *testing.T) { + input := `{zoona: 1, bodza: 2}` + + l := lexer.New([]byte(input)) + p := New(l) + program := p.ParseProgram() + + checkParserErrors(t, p) + + statement, ok := program.Statements[0].(*ast.ExpressionStatement) + + if !ok { + t.Fatalf("program.Statements[0] is not ast.Expression. got=%T", program.Statements[0]) } + mapLiteral, ok := statement.Expression.(*ast.MapExpression) + + if !ok { + t.Fatalf("statement is not ast.Map. got=%T", statement.Expression) + } + + if len(mapLiteral.Pairs) != 2 { + t.Fatalf("map.Pairs has wrong length. got=%d", len(mapLiteral.Pairs)) + } + + expected := map[bool]int64{ + true: 1, + false: 2, + } + + for key, value := range mapLiteral.Pairs { + boolean, ok := key.(*ast.Boolean) + + if !ok { + t.Errorf("key is not ast.Boolean. got=%T", key) + } + + expectedValue := expected[boolean.Value] + + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestMapLiteralsWithIntegerKeys(t *testing.T) { + input := `{1: 1, 2: 2, 3: 3}` + + l := lexer.New([]byte(input)) + p := New(l) + program := p.ParseProgram() + + checkParserErrors(t, p) + + statement, ok := program.Statements[0].(*ast.ExpressionStatement) + + if !ok { + t.Fatalf("program.Statements[0] is not ast.Expression. got=%T", program.Statements[0]) + } + + mapLiteral, ok := statement.Expression.(*ast.MapExpression) + + if !ok { + t.Fatalf("statement is not ast.Map. got=%T", statement.Expression) + } + + if len(mapLiteral.Pairs) != 3 { + t.Fatalf("map.Pairs has wrong length. got=%d", len(mapLiteral.Pairs)) + } + + expected := map[int64]int64{ + 1: 1, + 2: 2, + 3: 3, + } + + for key, value := range mapLiteral.Pairs { + number, ok := key.(*ast.IntegerLiteral) + + if !ok { + t.Errorf("key is not ast.Number. got=%T", key) + } + + expectedValue := expected[number.Value.IntPart()] + + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestMapLiteralsWithVariableKeys(t *testing.T) { + input := `{foo: 1, bar: 2, baz: 3}` + + l := lexer.New([]byte(input)) + p := New(l) + program := p.ParseProgram() + + checkParserErrors(t, p) + + statement, ok := program.Statements[0].(*ast.ExpressionStatement) + + if !ok { + t.Fatalf("program.Statements[0] is not ast.Expression. got=%T", program.Statements[0]) + } + + mapLiteral, ok := statement.Expression.(*ast.MapExpression) + + if !ok { + t.Fatalf("statement is not ast.Map. got=%T", statement.Expression) + } + + if len(mapLiteral.Pairs) != 3 { + t.Fatalf("map.Pairs has wrong length. got=%d", len(mapLiteral.Pairs)) + } + + expected := map[string]int64{ + "foo": 1, + "bar": 2, + "baz": 3, + } + + for key, value := range mapLiteral.Pairs { + identifier, ok := key.(*ast.Identifier) + + if !ok { + t.Errorf("key is not ast.Identifier. got=%T", key) + } + + expectedValue := expected[identifier.Value] + + testIntegerLiteral(t, value, expectedValue) + } } diff --git a/src/token/token.go b/src/token/token.go index b3dc42a..3daaac0 100644 --- a/src/token/token.go +++ b/src/token/token.go @@ -94,6 +94,7 @@ var keywords = map[string]TokenType{ var variableTypes = map[TokenType]TokenType{ INTEGER: INTEGER, STRING: STRING, + MAP: MAP, } func LookupVariableType(ident TokenType) TokenType { From 876e079fe8c0cc619592b71abdc1d953bba7f7aa Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Mon, 29 Apr 2024 23:47:07 +0200 Subject: [PATCH 4/7] init map evaluator --- src/evaluator/evaluator.go | 2 + src/evaluator/evaluator_test.go | 84 +++++++++++++++++++++++++++++++++ src/evaluator/map.go | 46 ++++++++++++++++++ src/object/boolean.go | 12 ++++- src/object/integer.go | 11 +++++ src/object/map.go | 38 +++++++++++++++ src/object/object.go | 9 ++++ src/object/object_test.go | 19 ++++++++ src/object/string.go | 10 ++++ src/parser/parser_test.go | 40 ++++++++++++++++ 10 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/evaluator/map.go create mode 100644 src/object/map.go create mode 100644 src/object/object_test.go diff --git a/src/evaluator/evaluator.go b/src/evaluator/evaluator.go index f0e719c..29ec34b 100644 --- a/src/evaluator/evaluator.go +++ b/src/evaluator/evaluator.go @@ -70,6 +70,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object { // Expressions case *ast.IntegerLiteral: return &object.Integer{Value: node.Value} + case *ast.MapExpression: + return evalMapExpression(node, env) case *ast.Boolean: return nativeBoolToBooleanObject(node.Value) case *ast.StringLiteral: diff --git a/src/evaluator/evaluator_test.go b/src/evaluator/evaluator_test.go index 1d62a31..52feae9 100644 --- a/src/evaluator/evaluator_test.go +++ b/src/evaluator/evaluator_test.go @@ -313,6 +313,10 @@ func TestErrorHandling(t *testing.T) { `"Hello"- "World"`, "unknown operator: STRING - STRING", }, + { + `{"name": "Monkey"}[ndondomeko(x) { x }];`, + "unusable as hash key: FUNCTION", + }, } for _, tt := range tests { evaluated := testEval(tt.input) @@ -593,3 +597,83 @@ func TestMethodCalls(t *testing.T) { } } } + +func TestHashLiterals(t *testing.T) { + input := `mawu two = "two"; + { + "one": 10- 9, + two: 1 + 1, + "thr" + "ee": 6 / 2, + 4: 4, + zoona: 5, + bodza: 6 + }` + evaluated := testEval(input) + result, ok := evaluated.(*object.Map) + if !ok { + t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated) + } + expected := map[object.MapKey]int64{ + (&object.String{Value: "one"}).MapKey(): 1, + (&object.String{Value: "two"}).MapKey(): 2, + (&object.String{Value: "three"}).MapKey(): 3, + (&object.Integer{Value: decimal.NewFromInt(4)}).MapKey(): 4, + TRUE.MapKey(): 5, + FALSE.MapKey(): 6, + } + if len(result.Pairs) != len(expected) { + t.Fatalf("Hash has 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") + } + testLiteralExpression(t, pair.Value, expectedValue) + } +} + +func TestHashIndexExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + `{"foo": 5}["foo"]`, + 5, + }, + { + `{"foo": 5}["bar"]`, + nil, + }, + { + `mawu key = "foo"; {"foo": 5}[key]`, + 5, + }, + { + `{}["foo"]`, + nil, + }, + { + `{5: 5}[5]`, + 5, + }, + { + `{zoona: 5}[zoona]`, + 5, + }, + { + `{bodza: 5}[bodza]`, + 5, + }, + } + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testLiteralExpression(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} diff --git a/src/evaluator/map.go b/src/evaluator/map.go new file mode 100644 index 0000000..66457c7 --- /dev/null +++ b/src/evaluator/map.go @@ -0,0 +1,46 @@ +package evaluator + +import ( + "github.com/sevenreup/chewa/src/ast" + "github.com/sevenreup/chewa/src/object" +) + +func evalMapExpression(node *ast.MapExpression, + env *object.Environment) object.Object { + pairs := make(map[object.MapKey]object.MapPair) + + for keyNode, valueNode := range node.Pairs { + identifier, ok := keyNode.(*ast.Identifier) + + if ok { + keyNode = &ast.StringLiteral{ + Token: identifier.Token, + Value: identifier.Value, + } + } + + key := Eval(keyNode, env) + + if isError(key) { + return key + } + + mapKey, ok := key.(object.Mappable) + + if !ok { + return newError("%d:%d:%s: runtime error: unusable as map key: %s", node.Token.Pos.Line, node.Token.Pos.Column, node.Token.File, key.Type()) + } + + value := Eval(valueNode, env) + + if isError(value) { + return value + } + + hashed := mapKey.MapKey() + + pairs[hashed] = object.MapPair{Key: key, Value: value} + } + + return &object.Map{Pairs: pairs} +} diff --git a/src/object/boolean.go b/src/object/boolean.go index 5f87739..4d170fc 100644 --- a/src/object/boolean.go +++ b/src/object/boolean.go @@ -1,11 +1,15 @@ package object -import "fmt" +import ( + "fmt" + "hash/fnv" +) const BOOLEAN_OBJ = "BOOLEAN" type Boolean struct { Object + Mappable Value bool } @@ -17,3 +21,9 @@ func (i *Boolean) Method(method string, args []Object) (Object, bool) { //TODO implement me panic("implement me") } + +func (s *Boolean) MapKey() MapKey { + h := fnv.New64a() + h.Write([]byte(fmt.Sprint(s.Value))) + return MapKey{Type: s.Type(), Value: h.Sum64()} +} diff --git a/src/object/integer.go b/src/object/integer.go index 1fb820f..f2fa8a0 100644 --- a/src/object/integer.go +++ b/src/object/integer.go @@ -1,12 +1,17 @@ package object import ( + "fmt" + "hash/fnv" + "github.com/shopspring/decimal" ) const INTEGER_OBJ = "INTEGER" type Integer struct { + Object + Mappable Value decimal.Decimal } @@ -18,3 +23,9 @@ func (i *Integer) Method(method string, args []Object) (Object, bool) { //TODO implement me panic("implement me") } + +func (s *Integer) MapKey() MapKey { + h := fnv.New64a() + h.Write([]byte(fmt.Sprint(s.Value))) + return MapKey{Type: s.Type(), Value: h.Sum64()} +} diff --git a/src/object/map.go b/src/object/map.go new file mode 100644 index 0000000..77c850f --- /dev/null +++ b/src/object/map.go @@ -0,0 +1,38 @@ +package object + +import ( + "bytes" + "fmt" + "strings" +) + +const MAP_OBJ = "MAP" + +type Map struct { + Pairs map[MapKey]MapPair +} + +type MapPair struct { + Key Object + Value Object +} + +func (m *Map) Type() ObjectType { return STRING_OBJ } + +func (m *Map) Inspect() string { + var out bytes.Buffer + pairs := []string{} + for _, pair := range m.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() +} + +func (m *Map) Method(method string, args []Object) (Object, bool) { + //TODO implement me + panic("implement me") +} diff --git a/src/object/object.go b/src/object/object.go index 05bfb93..360d561 100644 --- a/src/object/object.go +++ b/src/object/object.go @@ -10,6 +10,15 @@ type Object interface { Inspect() string } +type MapKey struct { + Type ObjectType + Value uint64 +} + +type Mappable interface { + MapKey() MapKey +} + type HasMethods interface { Method(method string, args []Object) (Object, bool) } diff --git a/src/object/object_test.go b/src/object/object_test.go new file mode 100644 index 0000000..1c5f7c7 --- /dev/null +++ b/src/object/object_test.go @@ -0,0 +1,19 @@ +package object + +import "testing" + +func TestStringMapKey(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.MapKey() != hello2.MapKey() { + t.Errorf("strings with same content have different hash keys") + } + if diff1.MapKey() != diff2.MapKey() { + t.Errorf("strings with same content have different hash keys") + } + if hello1.MapKey() == diff1.MapKey() { + t.Errorf("strings with different content have same hash keys") + } +} diff --git a/src/object/string.go b/src/object/string.go index eed8e27..fe98321 100644 --- a/src/object/string.go +++ b/src/object/string.go @@ -1,8 +1,12 @@ package object +import "hash/fnv" + const STRING_OBJ = "STRING" type String struct { + Object + Mappable Value string } @@ -14,3 +18,9 @@ func (i *String) Method(method string, args []Object) (Object, bool) { //TODO implement me panic("implement me") } + +func (s *String) MapKey() MapKey { + h := fnv.New64a() + h.Write([]byte(s.Value)) + return MapKey{Type: s.Type(), Value: h.Sum64()} +} diff --git a/src/parser/parser_test.go b/src/parser/parser_test.go index b2d767a..80e443c 100644 --- a/src/parser/parser_test.go +++ b/src/parser/parser_test.go @@ -1303,3 +1303,43 @@ func TestMapLiteralsWithVariableKeys(t *testing.T) { testIntegerLiteral(t, value, expectedValue) } } + +func TestParsingHashLiteralsWithExpressions(t *testing.T) { + input := `{"one": 0 + 1, "two": 10- 8, "three": 15 / 5}` + l := lexer.New([]byte(input)) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.MapExpression) + if !ok { + t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) + } + if len(hash.Pairs) != 3 { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + tests := map[string]func(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) + } +} From aba37cafe6067824e9415ef43dbdd6d7b9b286df Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Mon, 6 May 2024 23:41:13 +0200 Subject: [PATCH 5/7] will merge --- src/evaluator/evaluator_test.go | 86 ++++++++++++++++----------------- src/evaluator/index.go | 20 ++++++++ src/object/map.go | 2 +- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/evaluator/evaluator_test.go b/src/evaluator/evaluator_test.go index 52feae9..170d869 100644 --- a/src/evaluator/evaluator_test.go +++ b/src/evaluator/evaluator_test.go @@ -270,49 +270,49 @@ func TestErrorHandling(t *testing.T) { input string expectedMessage string }{ - { - "5 + zoona;", - "type mismatch: INTEGER + BOOLEAN", - }, - { - "5 + zoona; 5;", - "type mismatch: INTEGER + BOOLEAN", - }, - { - "-zoona", - "unknown operator:-BOOLEAN", - }, - { - "zoona + bodza;", - "unknown operator: BOOLEAN + BOOLEAN", - }, - { - "5; zoona + bodza; 5", - "unknown operator: BOOLEAN + BOOLEAN", - }, - { - "ngati (10 > 1) { zoona + bodza; }", - "unknown operator: BOOLEAN + BOOLEAN", - }, - { - ` - ngati (10 > 1) { - ngati (10 > 1) { - bweza zoona + bodza; - } - bweza 1; - } - `, - "unknown operator: BOOLEAN + BOOLEAN", - }, - { - "foobar", - "identifier not found: foobar", - }, - { - `"Hello"- "World"`, - "unknown operator: STRING - STRING", - }, + // { + // "5 + zoona;", + // "type mismatch: INTEGER + BOOLEAN", + // }, + // { + // "5 + zoona; 5;", + // "type mismatch: INTEGER + BOOLEAN", + // }, + // { + // "-zoona", + // "unknown operator:-BOOLEAN", + // }, + // { + // "zoona + bodza;", + // "unknown operator: BOOLEAN + BOOLEAN", + // }, + // { + // "5; zoona + bodza; 5", + // "unknown operator: BOOLEAN + BOOLEAN", + // }, + // { + // "ngati (10 > 1) { zoona + bodza; }", + // "unknown operator: BOOLEAN + BOOLEAN", + // }, + // { + // ` + // ngati (10 > 1) { + // ngati (10 > 1) { + // bweza zoona + bodza; + // } + // bweza 1; + // } + // `, + // "unknown operator: BOOLEAN + BOOLEAN", + // }, + // { + // "foobar", + // "identifier not found: foobar", + // }, + // { + // `"Hello"- "World"`, + // "unknown operator: STRING - STRING", + // }, { `{"name": "Monkey"}[ndondomeko(x) { x }];`, "unusable as hash key: FUNCTION", diff --git a/src/evaluator/index.go b/src/evaluator/index.go index 4b483b5..49b48c6 100644 --- a/src/evaluator/index.go +++ b/src/evaluator/index.go @@ -18,6 +18,8 @@ func evalIndexExpression(node *ast.IndexExpression, env *object.Environment) obj switch { case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ: return evalArrayIndexExpression(left, index) + case left.Type() == object.MAP_OBJ: + return evaluateMapIndex(node, left, index) default: return newError("index operator not supported: %s", left.Type()) } @@ -34,3 +36,21 @@ func evalArrayIndexExpression(array, index object.Object) object.Object { return arrayObject.Elements[idx] } + +func evaluateMapIndex(node *ast.IndexExpression, left, index object.Object) object.Object { + mapObject := left.(*object.Map) + + key, ok := index.(object.Mappable) + + if !ok { + return newError("%d:%d:%s: runtime error: unusable as map key: %s", node.Token.Pos.Line, node.Token.Pos.Column, node.Token.File, index.Type()) + } + + pair, ok := mapObject.Pairs[key.MapKey()] + + if !ok { + return NULL + } + + return pair.Value +} diff --git a/src/object/map.go b/src/object/map.go index 77c850f..c67e4ce 100644 --- a/src/object/map.go +++ b/src/object/map.go @@ -17,7 +17,7 @@ type MapPair struct { Value Object } -func (m *Map) Type() ObjectType { return STRING_OBJ } +func (m *Map) Type() ObjectType { return MAP_OBJ } func (m *Map) Inspect() string { var out bytes.Buffer From e5d4ecda002cdcde58574f49b4cc02472de090db Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Wed, 15 May 2024 22:54:37 +0200 Subject: [PATCH 6/7] add hashmap examples --- examples/map.ny | 7 +++ src/evaluator/evaluator_test.go | 92 ++++++++++++++++----------------- 2 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 examples/map.ny diff --git a/examples/map.ny b/examples/map.ny new file mode 100644 index 0000000..3944ce8 --- /dev/null +++ b/examples/map.ny @@ -0,0 +1,7 @@ +mgwirizano munthu = { + "dzina": "Soma", + "zaka": 3 +} + +lembanzr(munthu["dzina"]) +lembanzr(munthu["zaka"]) \ No newline at end of file diff --git a/src/evaluator/evaluator_test.go b/src/evaluator/evaluator_test.go index 170d869..535802e 100644 --- a/src/evaluator/evaluator_test.go +++ b/src/evaluator/evaluator_test.go @@ -270,52 +270,52 @@ func TestErrorHandling(t *testing.T) { input string expectedMessage string }{ - // { - // "5 + zoona;", - // "type mismatch: INTEGER + BOOLEAN", - // }, - // { - // "5 + zoona; 5;", - // "type mismatch: INTEGER + BOOLEAN", - // }, - // { - // "-zoona", - // "unknown operator:-BOOLEAN", - // }, - // { - // "zoona + bodza;", - // "unknown operator: BOOLEAN + BOOLEAN", - // }, - // { - // "5; zoona + bodza; 5", - // "unknown operator: BOOLEAN + BOOLEAN", - // }, - // { - // "ngati (10 > 1) { zoona + bodza; }", - // "unknown operator: BOOLEAN + BOOLEAN", - // }, - // { - // ` - // ngati (10 > 1) { - // ngati (10 > 1) { - // bweza zoona + bodza; - // } - // bweza 1; - // } - // `, - // "unknown operator: BOOLEAN + BOOLEAN", - // }, - // { - // "foobar", - // "identifier not found: foobar", - // }, - // { - // `"Hello"- "World"`, - // "unknown operator: STRING - STRING", - // }, - { - `{"name": "Monkey"}[ndondomeko(x) { x }];`, - "unusable as hash key: FUNCTION", + { + "5 + zoona;", + "type mismatch: INTEGER + BOOLEAN", + }, + { + "5 + zoona; 5;", + "type mismatch: INTEGER + BOOLEAN", + }, + { + "-zoona", + "unknown operator:-BOOLEAN", + }, + { + "zoona + bodza;", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + "5; zoona + bodza; 5", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + "ngati (10 > 1) { zoona + bodza; }", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + ` + ngati (10 > 1) { + ngati (10 > 1) { + bweza zoona + bodza; + } + bweza 1; + } + `, + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + "foobar", + "identifier not found: foobar", + }, + { + `"Hello"- "World"`, + "unknown operator: STRING - STRING", + }, + { + `{"dzina": "Maliko"}[ndondomeko d(x) { x }];`, + "1:20:: runtime error: unusable as map key: FUNCTION", }, } for _, tt := range tests { From 0bebeb74192eaebbed26292029dcf1f42e3bafcf Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Wed, 15 May 2024 22:56:49 +0200 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa0c6e7..6eb5dea 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ You can check out examples in `./examples` folder to quickly see what the langua - [ ] Type checking (for now it does not strictly enforce types) - [ ] Data Structures - [x] arrays - - [ ] dictionaries + - [x] dictionaries - [ ] Input/ Output - [ ] Error Handling - [ ] Class support