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() }