Mako/parser/parser_test.go
2025-06-09 12:18:34 -05:00

408 lines
9.7 KiB
Go

package parser
import (
"strings"
"testing"
)
func TestLiterals(t *testing.T) {
tests := []struct {
input string
expected any
}{
{"42", 42.0},
{"3.14", 3.14},
{`"hello"`, "hello"},
{"true", true},
{"false", false},
{"nil", nil},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
// Parse as expression directly - literals are not valid statements
l := NewLexer(tt.input)
p := NewParser(l)
expr := p.parseExpression(LOWEST)
checkParserErrors(t, p)
switch expected := tt.expected.(type) {
case float64:
testNumberLiteral(t, expr, expected)
case string:
testStringLiteral(t, expr, expected)
case bool:
testBooleanLiteral(t, expr, expected)
case nil:
testNilLiteral(t, expr)
}
})
}
}
func TestAssignStatements(t *testing.T) {
tests := []struct {
input string
expectedIdentifier string
expectedValue any
isExpression bool // true if expectedValue is expression string representation
}{
{"x = 42", "x", 42.0, false},
{"name = \"test\"", "name", "test", false},
{"flag = true", "flag", true, false},
{"ptr = nil", "ptr", nil, false},
{"result = 3 + 4", "result", "(3.00 + 4.00)", true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
l := NewLexer(tt.input)
p := NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*AssignStatement)
if !ok {
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
}
if stmt.Name.Value != tt.expectedIdentifier {
t.Errorf("expected identifier %s, got %s", tt.expectedIdentifier, stmt.Name.Value)
}
if tt.isExpression {
// Test the string representation of the expression
if stmt.Value.String() != tt.expectedValue.(string) {
t.Errorf("expected expression %s, got %s", tt.expectedValue.(string), stmt.Value.String())
}
} else {
// Test the actual value based on type
switch expected := tt.expectedValue.(type) {
case float64:
testNumberLiteral(t, stmt.Value, expected)
case string:
testStringLiteral(t, stmt.Value, expected)
case bool:
testBooleanLiteral(t, stmt.Value, expected)
case nil:
testNilLiteral(t, stmt.Value)
}
}
})
}
}
func TestInfixExpressions(t *testing.T) {
tests := []struct {
input string
leftValue any
operator string
rightValue any
}{
{"5 + 5", 5.0, "+", 5.0},
{"5 - 5", 5.0, "-", 5.0},
{"5 * 5", 5.0, "*", 5.0},
{"5 / 5", 5.0, "/", 5.0},
{"true + false", true, "+", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
l := NewLexer(tt.input)
p := NewParser(l)
expr := p.parseExpression(LOWEST)
checkParserErrors(t, p)
testInfixExpression(t, expr, tt.leftValue, tt.operator, tt.rightValue)
})
}
}
func TestOperatorPrecedence(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"1 + 2 * 3", "(1.00 + (2.00 * 3.00))"},
{"2 * 3 + 4", "((2.00 * 3.00) + 4.00)"},
{"(1 + 2) * 3", "((1.00 + 2.00) * 3.00)"},
{"1 + 2 - 3", "((1.00 + 2.00) - 3.00)"},
{"2 * 3 / 4", "((2.00 * 3.00) / 4.00)"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
l := NewLexer(tt.input)
p := NewParser(l)
expr := p.parseExpression(LOWEST)
checkParserErrors(t, p)
if expr.String() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, expr.String())
}
})
}
}
func TestParsingErrors(t *testing.T) {
tests := []struct {
input string
expectedError string
line int
column int
}{
{"= 5", "assignment operator '=' without left-hand side identifier", 1, 1},
{"x =", "expected expression after assignment operator", 1, 3},
{"(1 + 2", "expected next token to be )", 1, 7},
{"+ 5", "unexpected operator '+'", 1, 1},
{"1 +", "expected expression after operator '+'", 1, 3},
{"@", "unexpected token '@'", 1, 1},
{"invalid@", "unexpected identifier", 1, 1},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
l := NewLexer(tt.input)
p := NewParser(l)
// Decide parsing strategy based on the type of error we're testing
switch tt.input {
case "(1 + 2", "+ 5", "1 +":
// These are expression-level errors
p.parseExpression(LOWEST)
default:
// These are statement-level errors
p.ParseProgram()
}
errors := p.Errors()
if len(errors) == 0 {
t.Fatalf("expected parsing errors, got none")
}
found := false
for _, err := range errors {
if strings.Contains(err.Message, tt.expectedError) {
found = true
if err.Line != tt.line {
t.Errorf("expected error at line %d, got line %d", tt.line, err.Line)
}
break
}
}
if !found {
errorMsgs := make([]string, len(errors))
for i, err := range errors {
errorMsgs[i] = err.Message
}
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
}
})
}
}
func TestErrorRecovery(t *testing.T) {
input := `x = 42
= 5
y = "hello"`
l := NewLexer(input)
p := NewParser(l)
program := p.ParseProgram()
// Should have errors but still parse valid statements
if !p.HasErrors() {
t.Fatal("expected parsing errors")
}
errors := p.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, "assignment operator '=' without left-hand side identifier") {
found = true
if err.Line != 2 {
t.Errorf("expected error at line 2, got line %d", err.Line)
}
break
}
}
if !found {
t.Error("expected specific assignment error")
}
// Should still have parsed the valid statements
validStatements := 0
for _, stmt := range program.Statements {
if stmt != nil {
validStatements++
}
}
if validStatements < 2 {
t.Errorf("expected at least 2 valid statements, got %d", validStatements)
}
}
func TestProgram(t *testing.T) {
input := `x = 42
y = "hello"
z = true + false`
l := NewLexer(input)
p := NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 3 {
t.Fatalf("expected 3 statements, got %d", len(program.Statements))
}
expectedIdentifiers := []string{"x", "y", "z"}
for i, expectedIdent := range expectedIdentifiers {
stmt, ok := program.Statements[i].(*AssignStatement)
if !ok {
t.Fatalf("statement %d: expected AssignStatement, got %T", i, program.Statements[i])
}
if stmt.Name.Value != expectedIdent {
t.Errorf("statement %d: expected identifier %s, got %s", i, expectedIdent, stmt.Name.Value)
}
}
}
func TestErrorMessages(t *testing.T) {
tests := []struct {
input string
expectedMessage string
}{
{"= 5", "Parse error at line 1, column 1: assignment operator '=' without left-hand side identifier (near '=')"},
{"x =", "Parse error at line 1, column 3: expected expression after assignment operator (near '')"},
{"(", "Parse error at line 1, column 1: unexpected end of input (near '')"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
l := NewLexer(tt.input)
p := NewParser(l)
p.ParseProgram()
if !p.HasErrors() {
t.Fatal("expected parsing errors")
}
errors := p.Errors()
if len(errors) == 0 {
t.Fatal("expected at least one error")
}
errorStr := errors[0].Error()
if !strings.Contains(errorStr, "Parse error at line") {
t.Errorf("expected formatted error message, got: %s", errorStr)
}
})
}
}
// Helper functions for testing specific node types
func testNumberLiteral(t *testing.T, expr Expression, expected float64) {
t.Helper()
num, ok := expr.(*NumberLiteral)
if !ok {
t.Fatalf("expected NumberLiteral, got %T", expr)
}
if num.Value != expected {
t.Errorf("expected %f, got %f", expected, num.Value)
}
}
func testStringLiteral(t *testing.T, expr Expression, expected string) {
t.Helper()
str, ok := expr.(*StringLiteral)
if !ok {
t.Fatalf("expected StringLiteral, got %T", expr)
}
if str.Value != expected {
t.Errorf("expected %s, got %s", expected, str.Value)
}
}
func testBooleanLiteral(t *testing.T, expr Expression, expected bool) {
t.Helper()
boolean, ok := expr.(*BooleanLiteral)
if !ok {
t.Fatalf("expected BooleanLiteral, got %T", expr)
}
if boolean.Value != expected {
t.Errorf("expected %t, got %t", expected, boolean.Value)
}
}
func testNilLiteral(t *testing.T, expr Expression) {
t.Helper()
_, ok := expr.(*NilLiteral)
if !ok {
t.Fatalf("expected NilLiteral, got %T", expr)
}
}
func testIdentifier(t *testing.T, expr Expression, expected string) {
t.Helper()
ident, ok := expr.(*Identifier)
if !ok {
t.Fatalf("expected Identifier, got %T", expr)
}
if ident.Value != expected {
t.Errorf("expected %s, got %s", expected, ident.Value)
}
}
func testInfixExpression(t *testing.T, expr Expression, left any, operator string, right any) {
t.Helper()
infix, ok := expr.(*InfixExpression)
if !ok {
t.Fatalf("expected InfixExpression, got %T", expr)
}
if infix.Operator != operator {
t.Errorf("expected operator %s, got %s", operator, infix.Operator)
}
switch leftVal := left.(type) {
case float64:
testNumberLiteral(t, infix.Left, leftVal)
case string:
testIdentifier(t, infix.Left, leftVal)
case bool:
testBooleanLiteral(t, infix.Left, leftVal)
}
switch rightVal := right.(type) {
case float64:
testNumberLiteral(t, infix.Right, rightVal)
case string:
testIdentifier(t, infix.Right, rightVal)
case bool:
testBooleanLiteral(t, infix.Right, rightVal)
}
}
func checkParserErrors(t *testing.T, p *Parser) {
t.Helper()
errors := p.Errors()
if len(errors) == 0 {
return
}
t.Errorf("parser has %d errors", len(errors))
for _, err := range errors {
t.Errorf("parser error: %s", err.Error())
}
t.FailNow()
}