Skip to content

Commit

Permalink
FEAT: lexing and parsing hash literals;
Browse files Browse the repository at this point in the history
190/210 @ interpreter pdf;
13/284 @ compiler pdf
  • Loading branch information
MKaczkow committed Oct 21, 2024
1 parent 932aad3 commit b4f673a
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 5 deletions.
2 changes: 1 addition & 1 deletion monkey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Repo for basic tutorial-based Golang study
## adding new type checklist
* [ ] add new token type (in `token/token.go`) to convert stream of characters into stream of tokens
* [ ] define token type
* [ ] add branch in `NextToken()` function, calling new function
* [ ] add branch in `NextToken()` (in `lexer/lexer.go`) function, calling new function
* [ ] add function to actually convert characters into tokens of given type
* [ ] add parsing logic to convert stream of tokens into AST (Abstract Syntax Tree)
* [ ] define node (in `ast/ast.go`)
Expand Down
22 changes: 22 additions & 0 deletions monkey/interpreter/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,25 @@ func (ce *CallExpression) String() string {

return out.String()
}

type HashLiteral struct {
Token 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 key, value := range hl.Pairs {
pairs = append(pairs, key.String()+":"+value.String())
}

out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")

return out.String()
}
6 changes: 6 additions & 0 deletions monkey/interpreter/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ func (l *Lexer) NextToken() token.Token {
tok = newToken(token.LBRACKET, l.ch)
case ']':
tok = newToken(token.RBRACKET, l.ch)
// remember that this character: ' is different
// from this character: " in Go
// ' means a single character (rune)
// " means a string
case ':':
tok = newToken(token.COLON, l.ch)
case 0:
tok.Literal = ""
tok.Type = token.EOF
Expand Down
6 changes: 6 additions & 0 deletions monkey/interpreter/lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ if (5 < 10) {
"foobar"
"foo bar"
[1, 2];
{"foo": "bar"}
`

tests := []struct {
Expand Down Expand Up @@ -152,6 +153,11 @@ if (5 < 10) {
{token.INT, "2"},
{token.RBRACKET, "]"},
{token.SEMICOLON, ";"},
{token.LBRACE, "{"},
{token.STRING, "foo"},
{token.COLON, ":"},
{token.STRING, "bar"},
{token.RBRACE, "}"},
{token.EOF, ""},
}

Expand Down
42 changes: 38 additions & 4 deletions monkey/interpreter/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,12 @@ func New(l *lexer.Lexer) *Parser {
p.registerPrefix(token.FALSE, p.parseBoolean)
p.registerPrefix(token.LPAREN, p.parseGroupedExpression)
p.registerPrefix(token.IF, p.parseIfExpression)
p.registerPrefix(token.FUNCTION, p.parseFuncionLiteral)
p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral)
p.registerPrefix(token.STRING, p.parseStringLiteral)
// arrays and hashes are treated similarly - opening '[' or '{'
// is parsed as "prefix" to expression and then the rest is parsed
p.registerPrefix(token.LBRACKET, p.parseArrayLiteral)
p.registerPrefix(token.LBRACE, p.parseHashLiteral)

// Deal with infix expressions
p.infixParseFns = make(map[token.TokenType]infixParseFn)
Expand Down Expand Up @@ -343,14 +346,14 @@ func (p *Parser) parseStringLiteral() ast.Expression { // string is expression,
return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal}
}

func (p *Parser) parseFuncionLiteral() ast.Expression {
func (p *Parser) parseFunctionLiteral() ast.Expression {
lit := &ast.FunctionLiteral{Token: p.curToken}

if !p.expectPeek(token.LPAREN) {
return nil
}

lit.Parameters = p.parseFuncionParameters()
lit.Parameters = p.parseFunctionParameters()

if !p.expectPeek(token.LBRACE) {
return nil
Expand Down Expand Up @@ -385,7 +388,7 @@ func (p *Parser) parseExpressionList(end token.TokenType) []ast.Expression {
return list
}

func (p *Parser) parseFuncionParameters() []*ast.Identifier {
func (p *Parser) parseFunctionParameters() []*ast.Identifier {
identifiers := []*ast.Identifier{}

if p.peekTokenIs(token.RPAREN) {
Expand Down Expand Up @@ -413,6 +416,37 @@ func (p *Parser) parseFuncionParameters() []*ast.Identifier {
return identifiers
}

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() // first element from pair - "key"
key := p.parseExpression(LOWEST)

if !p.expectPeek(token.COLON) {
return nil
}

p.nextToken() // second element from pair - "value"
value := p.parseExpression(LOWEST)

hash.Pairs[key] = value

// next token should be either a comma or a closing brace
// or the hash is malformed
if !p.peekTokenIs(token.RBRACE) && !p.expectPeek(token.COMMA) {
return nil
}
}

if !p.expectPeek(token.RBRACE) {
return nil
}

return hash
}

func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression {
exp := &ast.CallExpression{Token: p.curToken, Function: function}
exp.Arguments = p.parseExpressionList(token.RPAREN)
Expand Down
85 changes: 85 additions & 0 deletions monkey/interpreter/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,91 @@ func TestStringLiteralExpression(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 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))
}
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 is 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 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)
}
}

func testLetStatement(t *testing.T, s ast.Statement, name string) bool {
if s.TokenLiteral() != "let" {
t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral())
Expand Down
1 change: 1 addition & 0 deletions monkey/interpreter/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
// Delimiters
COMMA = ","
SEMICOLON = ";"
COLON = ":"

LPAREN = "("
RPAREN = ")"
Expand Down

0 comments on commit b4f673a

Please sign in to comment.