From 5fa7b2481aef9331f686f949cde90861d90e1c82 Mon Sep 17 00:00:00 2001 From: jmug Date: Sat, 4 Jan 2025 17:56:10 -0800 Subject: [PATCH] Parses infix expressions. Signed-off-by: jmug --- pkg/ast/ast.go | 17 +++++- pkg/parser/parser.go | 35 ++++++++++- pkg/parser/parser_test.go | 124 ++++++++++++++++++++++++++++++++++++++ pkg/parser/precedence.go | 29 +++++++++ 4 files changed, 201 insertions(+), 4 deletions(-) diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go index f60bd7b..08b1ac7 100644 --- a/pkg/ast/ast.go +++ b/pkg/ast/ast.go @@ -131,7 +131,7 @@ func (il *IntegerLiteral) String() string { } type PrefixExpression struct { - Token token.Token + Token token.Token // The operator token Operator string Right Expression } @@ -143,3 +143,18 @@ func (pe *PrefixExpression) TokenLiteral() string { func (pe *PrefixExpression) String() string { return "(" + pe.Operator + pe.Right.String() + ")" } + +type InfixExpression struct { + Token token.Token // The operator token + Operator string + Left Expression + Right Expression +} + +func (ie *InfixExpression) expressionNode() {} +func (ie *InfixExpression) TokenLiteral() string { + return ie.Token.Literal +} +func (ie *InfixExpression) String() string { + return "(" + ie.Left.String() + " " + ie.Operator + " " + ie.Right.String() + ")" +} diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index a96b23a..2fec255 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -30,10 +30,20 @@ func New(l *lexer.Lexer) *Parser { prefixParseFns: map[token.TokenType]prefixParseFn{}, infixParseFns: map[token.TokenType]infixParseFn{}, } + // Prefix registrations p.registerPrefix(token.IDENT, p.parseIdentifier) p.registerPrefix(token.INT, p.parseIntegerLiteral) p.registerPrefix(token.MINUS, p.parsePrefixExpression) p.registerPrefix(token.BANG, p.parsePrefixExpression) + // Infix registrations + p.registerInfix(token.PLUS, p.parseInfixExpression) + p.registerInfix(token.MINUS, p.parseInfixExpression) + p.registerInfix(token.ASTERISK, p.parseInfixExpression) + p.registerInfix(token.SLASH, p.parseInfixExpression) + p.registerInfix(token.GT, p.parseInfixExpression) + p.registerInfix(token.LT, p.parseInfixExpression) + p.registerInfix(token.EQ, p.parseInfixExpression) + p.registerInfix(token.NOT_EQ, p.parseInfixExpression) // TODO: figure out why this can't be done from `parseProgram` p.nextToken() p.nextToken() @@ -111,9 +121,16 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { p.noPrefixParseFnError(p.curToken.Type) return nil } - leftExpr := prefix() - return leftExpr - + curExpr := prefix() + for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() { + infix := p.infixParseFns[p.peekToken.Type] + if infix == nil { + return curExpr + } + p.nextToken() + curExpr = infix(curExpr) + } + return curExpr } func (p *Parser) parseIdentifier() ast.Expression { @@ -141,6 +158,18 @@ func (p *Parser) parsePrefixExpression() ast.Expression { return exp } +func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { + exp := &ast.InfixExpression{ + Token: p.curToken, + Operator: p.curToken.Literal, + Left: left, + } + precedence := p.curPrecedence() + p.nextToken() + exp.Right = p.parseExpression(precedence) + return exp +} + func (p *Parser) curTokenIs(typ token.TokenType) bool { return p.curToken.Type == typ } diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 2e6fbdf..48002f3 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -200,6 +200,130 @@ func TestParsingPrefixExpressions(t *testing.T) { } } +// parser/parser_test.go + +func TestParsingInfixExpressions(t *testing.T) { + infixTests := []struct { + input string + leftValue int64 + operator string + rightValue int64 + }{ + {"5 + 5;", 5, "+", 5}, + {"5 - 5;", 5, "-", 5}, + {"5 * 5;", 5, "*", 5}, + {"5 / 5;", 5, "/", 5}, + {"5 > 5;", 5, ">", 5}, + {"5 < 5;", 5, "<", 5}, + {"5 == 5;", 5, "==", 5}, + {"5 != 5;", 5, "!=", 5}, + } + + for _, tt := range infixTests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.InfixExpression) + if !ok { + t.Fatalf("exp is not ast.InfixExpression. got=%T", stmt.Expression) + } + + if !testIntegerLiteral(t, exp.Left, tt.leftValue) { + return + } + + if exp.Operator != tt.operator { + t.Fatalf("exp.Operator is not '%s'. got=%s", + tt.operator, exp.Operator) + } + + if !testIntegerLiteral(t, exp.Right, tt.rightValue) { + return + } + } +} + +func TestOperatorPrecedenceParsing(t *testing.T) { + tests := []struct { + input string + expected string + }{ + { + "-a * b", + "((-a) * b)", + }, + { + "!-a", + "(!(-a))", + }, + { + "a + b + c", + "((a + b) + c)", + }, + { + "a + b - c", + "((a + b) - c)", + }, + { + "a * b * c", + "((a * b) * c)", + }, + { + "a * b / c", + "((a * b) / c)", + }, + { + "a + b / c", + "(a + (b / c))", + }, + { + "a + b * c + d / e - f", + "(((a + (b * c)) + (d / e)) - f)", + }, + { + "3 + 4; -5 * 5", + "(3 + 4)((-5) * 5)", + }, + { + "5 > 4 == 3 < 4", + "((5 > 4) == (3 < 4))", + }, + { + "5 < 4 != 3 > 4", + "((5 < 4) != (3 > 4))", + }, + { + "3 + 4 * 5 == 3 * 1 + 4 * 5", + "((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))", + }, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + actual := program.String() + if actual != tt.expected { + t.Errorf("expected=%q, got=%q", tt.expected, actual) + } + } +} + func testIntegerLiteral(t *testing.T, il ast.Expression, value int64) bool { integ, ok := il.(*ast.IntegerLiteral) if !ok { diff --git a/pkg/parser/precedence.go b/pkg/parser/precedence.go index a9e935f..595bfb5 100644 --- a/pkg/parser/precedence.go +++ b/pkg/parser/precedence.go @@ -1,5 +1,9 @@ package parser +import ( + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + const ( _ int = iota LOWEST @@ -10,3 +14,28 @@ const ( PREFIX // -X or !X CALL // myFunction(X) ) + +var precedences = map[token.TokenType]int{ + token.EQ: EQUALS, + token.NOT_EQ: EQUALS, + token.GT: LESSGREATER, + token.LT: LESSGREATER, + token.PLUS: SUM, + token.MINUS: SUM, + token.ASTERISK: PRODUCT, + token.SLASH: PRODUCT, +} + +func (p *Parser) peekPrecedence() int { + if pr, ok := precedences[p.peekToken.Type]; ok { + return pr + } + return LOWEST +} + +func (p *Parser) curPrecedence() int { + if pr, ok := precedences[p.curToken.Type]; ok { + return pr + } + return LOWEST +}