From 45e24c8426614f6957cd843ae53d507196840cff Mon Sep 17 00:00:00 2001 From: Matthew Grayson Date: Tue, 24 Mar 2026 12:10:55 -0500 Subject: [PATCH 1/7] feat(ast): add CallExpression AST node - Add CallExpression to ast/ast.go - Fields: Token token.Token, Function Expression, Arguments []Expression - Implements expressionNode(), TokenLiteral() and String() to render calls like fn(arg1, arg2) --- ast/ast.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/ast/ast.go b/ast/ast.go index 09fd6bf..07695d5 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -50,6 +50,30 @@ func (b *Boolean) expressionNode() {} func (b *Boolean) TokenLiteral() string { return b.Token.Literal } func (b *Boolean) String() string { return b.Token.Literal } +type CallExpression struct { + Token token.Token + Function Expression + Arguments []Expression +} + +func (ce *CallExpression) expressionNode() {} +func (ce *CallExpression) TokenLiteral() string { return ce.Token.Literal } +func (ce *CallExpression) String() string { + var out bytes.Buffer + + args := []string{} + for _, a := range ce.Arguments { + args = append(args, a.String()) + } + + out.WriteString(ce.Function.String()) + out.WriteString("(") + out.WriteString(strings.Join(args, ", ")) + out.WriteString(")") + + return out.String() +} + type FunctionLiteral struct { Token token.Token Parameters []*Identifier From 3f834f2ab127451b3ddabdef19f3113f76e9fc75 Mon Sep 17 00:00:00 2001 From: Matthew Grayson Date: Fri, 27 Mar 2026 13:04:33 -0500 Subject: [PATCH 2/7] test(parser): add TestCallExpressionParsing for call expressions --- parser/parser_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/parser/parser_test.go b/parser/parser_test.go index a9426a6..a39ac0f 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -45,6 +45,41 @@ func TestBooleanExpression(t *testing.T) { } } +func TestCallExpressionParsing(t *testing.T) { + input := "add(1, 2 * 3, 4 + 5);" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program does not contain %d statements. got=%d", 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("stmt is not ast.ExpressionStatement. got=%T", program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.CallExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.Call Expression. got=%T", stmt.Expression) + } + + if !testIdentifier(t, exp.Function, "add") { + return + } + + if len(exp.Arguments) != 3 { + t.Fatalf("wrong number of arguments. got=%d", len(exp.Arguments)) + } + + testLiteralExpression(t, exp.Arguments[0], 1) + testInfixExpression(t, exp.Arguments[1], 2, "*", 3) + testInfixExpression(t, exp.Arguments[2], 4, "+", 5) +} + func TestIdentifierExpression(t *testing.T) { input := "foobar;" From 2458b71db639439c9508859717c960da44bbb0fd Mon Sep 17 00:00:00 2001 From: Matthew Grayson Date: Thu, 2 Apr 2026 15:55:44 -0500 Subject: [PATCH 3/7] feat(parser): add call-expression parsing and tests - Register LPAREN as a call-expression infix so function-call sites are parsed correctly - Add parseCallExpression and parseCallArguments to build call AST nodes and consume arguments - Add/update tests in parser/parser_test.go to verify call parsing and operator precedence inside arguments --- parser/parser.go | 32 ++++++++++++++++++++++++++++++++ parser/parser_test.go | 12 ++++++++++++ 2 files changed, 44 insertions(+) diff --git a/parser/parser.go b/parser/parser.go index fa3fca9..2735e88 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -30,6 +30,7 @@ var precedences = map[token.TokenType]int{ token.MINUS: SUM, token.SLASH: PRODUCT, token.ASTERISK: PRODUCT, + token.LPAREN: CALL, } type Parser struct { @@ -74,6 +75,7 @@ func New(l *lexer.Lexer) *Parser { p.registerInfix(token.NOT_EQ, p.parseInfixExpression) p.registerInfix(token.LT, p.parseInfixExpression) p.registerInfix(token.GT, p.parseInfixExpression) + p.registerInfix(token.LPAREN, p.parseCallExpression) // read two tokens to set both curToken and peekToken p.nextToken() @@ -116,6 +118,36 @@ func (p *Parser) nextToken() { p.peekToken = p.l.NextToken() } +func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression { + exp := &ast.CallExpression{Token: p.curToken, Function: function} + exp.Arguments = p.parseCallArguments() + return exp +} + +func (p *Parser) parseCallArguments() []ast.Expression { + args := []ast.Expression{} + + if p.peekTokenIs(token.RPAREN) { + p.nextToken() + return args + } + + p.nextToken() + args = append(args, p.parseExpression(LOWEST)) + + for p.peekTokenIs(token.COMMA) { + p.nextToken() + p.nextToken() + args = append(args, p.parseExpression(LOWEST)) + } + + if !p.expectPeek(token.RPAREN) { + return nil + } + + return args +} + func (p *Parser) ParseProgram() *ast.Program { program := &ast.Program{} program.Statements = []ast.Statement{} diff --git a/parser/parser_test.go b/parser/parser_test.go index a39ac0f..69615fb 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -579,6 +579,18 @@ func TestOperatorPrecedenceParsing(t *testing.T) { "!(true == true)", "(!(true == true))", }, + { + "a + add(b * c) + d", + "((a + add((b * c))) + d)", + }, + { + "add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))", + "add(a, b, 1, (2 * 3), (4 + 5), add(6, (7 * 8)))", + }, + { + "add(a + b + c * d / f + g)", + "add((((a + b) + ((c * d) / f)) + g))", + }, } for _, tt := range tests { l := lexer.New(tt.input) From f81debcbd332f00357ef83eb87dcc02f556b2613 Mon Sep 17 00:00:00 2001 From: Matthew Grayson Date: Fri, 8 May 2026 16:03:51 -0500 Subject: [PATCH 4/7] test(parser): extend let statement tests for assigned values * Convert let statement cases to table-driven inputs * Add expected values for integer, boolean, and identifier assignments --- parser/parser_test.go | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/parser/parser_test.go b/parser/parser_test.go index 69615fb..254766b 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -606,35 +606,38 @@ func TestOperatorPrecedenceParsing(t *testing.T) { } func TestLetStatements(t *testing.T) { - input := ` -let x = 5; -let y = 10; -let foobar = 838383; -` - - l := lexer.New(input) - p := New(l) - - program := p.ParseProgram() - checkParserErrors(t, p) - if program == nil { - t.Fatalf("ParseProgram() returned nil") - } - tests := []struct { + input string expectedIdentifier string + expectedValue interface{} }{ - {"x"}, - {"y"}, - {"foobar"}, + {"let x = 5;", "x", 5}, + {"let y = true;", "y", true}, + {"let foobar = y;", "foobar", "y"}, } - for i, tt := range tests { - stmt := program.Statements[i] + for _, tt := range tests { + 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 1 statement. got=%d", + len(program.Statements)) + } + + stmt := program.Statements[0] if !testLetStatement(t, stmt, tt.expectedIdentifier) { return } + + val := stmt.(*ast.LetStatement).Value + if !testLiteralExpression(t, val, tt.expectedValue) { + return + } } + } func testLetStatement(t *testing.T, s ast.Statement, name string) bool { From a4ab36829f7d72d124cdc1916b2bef7f26881cda Mon Sep 17 00:00:00 2001 From: Matthew Grayson Date: Tue, 12 May 2026 12:48:54 -0500 Subject: [PATCH 5/7] feat(parser): parse let and return statement expressions * Replace placeholder token skipping with expression parsing in let and return statements. --- parser/parser.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 2735e88..7ed6ce8 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -348,7 +348,11 @@ func (p *Parser) parseLetStatement() *ast.LetStatement { if !p.expectPeek(token.ASSIGN) { return nil } - // skip expression parsing for now + + p.nextToken() + + stmt.Value = p.parseExpression(LOWEST) + for !p.curTokenIs(token.SEMICOLON) { p.nextToken() } @@ -359,7 +363,9 @@ func (p *Parser) parseReturnStatement() *ast.ReturnStatement { stmt := &ast.ReturnStatement{Token: p.curToken} p.nextToken() - // skip expression parsing for now + + stmt.ReturnValue = p.parseExpression(LOWEST) + for !p.curTokenIs(token.SEMICOLON) { p.nextToken() } From 34cfe567289f268d50ae0a8f5c7711c7b3582253 Mon Sep 17 00:00:00 2001 From: Matthew <106278637+Matthew-Grayson@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:31:35 -0500 Subject: [PATCH 6/7] fix typo in TestCallExpression error message Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- parser/parser_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/parser_test.go b/parser/parser_test.go index 254766b..b8495a1 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -64,7 +64,7 @@ func TestCallExpressionParsing(t *testing.T) { exp, ok := stmt.Expression.(*ast.CallExpression) if !ok { - t.Fatalf("stmt.Expression is not ast.Call Expression. got=%T", stmt.Expression) + t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", stmt.Expression) } if !testIdentifier(t, exp.Function, "add") { From fe097aac908cbb99401edca623111ef2a128ca43 Mon Sep 17 00:00:00 2001 From: Matthew <106278637+Matthew-Grayson@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:05:20 -0500 Subject: [PATCH 7/7] feat(parser): add EOF check to prevent infinite loop when checking for semicolon Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- parser/parser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 7ed6ce8..fe499f4 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -353,7 +353,7 @@ func (p *Parser) parseLetStatement() *ast.LetStatement { stmt.Value = p.parseExpression(LOWEST) - for !p.curTokenIs(token.SEMICOLON) { + for !p.curTokenIs(token.SEMICOLON) && !p.curTokenIs(token.EOF) { p.nextToken() } return stmt @@ -366,7 +366,7 @@ func (p *Parser) parseReturnStatement() *ast.ReturnStatement { stmt.ReturnValue = p.parseExpression(LOWEST) - for !p.curTokenIs(token.SEMICOLON) { + for !p.curTokenIs(token.SEMICOLON) && !p.curTokenIs(token.EOF) { p.nextToken() }