diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index b337242..b5d9d90 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -577,6 +577,36 @@ func TestHashLiterals(t *testing.T) { } } +func TestHashSelectorExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + `{"foo": 5}.foo`, + 5, + }, + { + `{"foo": 5}.bar`, + nil, + }, + { + `{}.foo`, + nil, + }, + } + + 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 TestHashIndexExpressions(t *testing.T) { tests := []struct { input string diff --git a/examples/selectors.monkey b/examples/selectors.monkey new file mode 100644 index 0000000..436f3fc --- /dev/null +++ b/examples/selectors.monkey @@ -0,0 +1,5 @@ +let d = {"foo": 1, "bar": 2} + +assert(d.foo == 1, "d.foo != 1") +assert(d.bar == 2, "d.bar != 2") +assert(d.bogus == null, "d.bogus != null") \ No newline at end of file diff --git a/lexer/lexer.go b/lexer/lexer.go index 1896363..6f8191e 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -107,6 +107,8 @@ func (l *Lexer) NextToken() token.Token { tok = newToken(token.SEMICOLON, l.ch) case ',': tok = newToken(token.COMMA, l.ch) + case '.': + tok = newToken(token.DOT, l.ch) case '(': tok = newToken(token.LPAREN, l.ch) case ')': diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 79e3f06..712787e 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -32,7 +32,7 @@ func TestNextToken(t *testing.T) { "foo bar" [1, 2]; {"foo": "bar"} - "foo \"bar\"" + d.foo ` tests := []struct { @@ -138,7 +138,9 @@ func TestNextToken(t *testing.T) { {token.COLON, ":"}, {token.STRING, "bar"}, {token.RBRACE, "}"}, - {token.STRING, "foo \"bar\""}, + {token.IDENT, "d"}, + {token.DOT, "."}, + {token.IDENT, "foo"}, {token.EOF, ""}, } diff --git a/parser/parser.go b/parser/parser.go index 2c59d51..b9376b0 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -35,6 +35,7 @@ var precedences = map[token.TokenType]int{ token.ASTERISK: PRODUCT, token.LPAREN: CALL, token.LBRACKET: INDEX, + token.DOT: INDEX, } type ( @@ -89,6 +90,7 @@ func New(l *lexer.Lexer) *Parser { p.registerInfix(token.LPAREN, p.parseCallExpression) p.registerInfix(token.LBRACKET, p.parseIndexExpression) p.registerInfix(token.ASSIGN, p.parseAssignmentExpression) + p.registerInfix(token.DOT, p.parseSelectorExpression) // Read two tokens, so curToken and peekToken are both set p.nextToken() @@ -570,3 +572,9 @@ func (p *Parser) parseComment() ast.Statement { func (p *Parser) parseNull() ast.Expression { return &ast.Null{Token: p.curToken} } + +func (p *Parser) parseSelectorExpression(expression ast.Expression) ast.Expression { + p.expectPeek(token.IDENT) + index := &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal} + return &ast.IndexExpression{Left: expression, Index: index} +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 1822a6e..57f09c8 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -358,6 +358,10 @@ func TestOperatorPrecedenceParsing(t *testing.T) { "add(a * b[2], b[1], 2 * [1, 2][1])", "add((a * (b[2])), (b[1]), (2 * ([1, 2][1])))", }, + { + "d.foo * d.bar", + "((d[foo]) * (d[bar]))", + }, } for _, tt := range tests { @@ -831,6 +835,40 @@ func TestParsingArrayLiterals(t *testing.T) { testInfixExpression(t, array.Elements[2], 3, "+", 3) } +func TestParsingSelectorExpressions(t *testing.T) { + input := "myHash.foo" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + t.Logf("stmt: %#v", stmt) + + exp, ok := stmt.Expression.(*ast.IndexExpression) + if !ok { + t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression) + } + + ident, ok := exp.Left.(*ast.Identifier) + if !ok { + t.Fatalf("exp.Left not *ast.Identifier. got=%T", stmt.Expression) + } + + if !testIdentifier(t, ident, "myHash") { + return + } + + index, ok := exp.Index.(*ast.StringLiteral) + if !ok { + t.Fatalf("exp.Index not *ast.StringLiteral. got=%T", stmt.Expression) + } + + if index.Value != "foo" { + t.Fatalf("index.Value != \"foo\"") + } +} + func TestParsingIndexExpressions(t *testing.T) { input := "myArray[1 + 1]" diff --git a/token/token.go b/token/token.go index 1427ab0..ad7ac78 100644 --- a/token/token.go +++ b/token/token.go @@ -39,6 +39,7 @@ const ( COMMA = "," SEMICOLON = ";" COLON = ":" + DOT = "." LPAREN = "(" RPAREN = ")" diff --git a/vm/vm_test.go b/vm/vm_test.go index 290712e..436f013 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -328,6 +328,16 @@ func TestHashLiterals(t *testing.T) { runVmTests(t, tests) } +func TestSelectorExpressions(t *testing.T) { + tests := []vmTestCase{ + {`{"foo": 5}.foo`, 5}, + {`{"foo": 5}.bar`, nil}, + {`{}.foo`, nil}, + } + + runVmTests(t, tests) +} + func TestIndexExpressions(t *testing.T) { tests := []vmTestCase{ {"[1, 2, 3][1]", 2},