From 8a8801f1c0b3cf7d9a044b882767a2707e7b8cc3 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 10 Jun 2025 09:36:17 -0500 Subject: [PATCH] remake tests --- go.sum | 2 - parser/ast.go | 49 ++ parser/parser.go | 98 ++- parser/parser_test.go | 1024 ----------------------------- parser/tests/assignments_test.go | 110 ++++ parser/tests/conditionals_test.go | 250 +++++++ parser/tests/errors_test.go | 140 ++++ parser/tests/expressions_test.go | 59 ++ parser/tests/helpers_test.go | 104 +++ parser/tests/literals_test.go | 41 ++ parser/tests/parser_test.go | 203 ++++++ parser/tests/strings_test.go | 87 +++ parser/tests/tables_test.go | 96 +++ parser/token.go | 16 +- 14 files changed, 1242 insertions(+), 1037 deletions(-) delete mode 100644 parser/parser_test.go create mode 100644 parser/tests/assignments_test.go create mode 100644 parser/tests/conditionals_test.go create mode 100644 parser/tests/errors_test.go create mode 100644 parser/tests/expressions_test.go create mode 100644 parser/tests/helpers_test.go create mode 100644 parser/tests/literals_test.go create mode 100644 parser/tests/parser_test.go create mode 100644 parser/tests/strings_test.go create mode 100644 parser/tests/tables_test.go diff --git a/go.sum b/go.sum index 115793b..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -git.sharkk.net/Go/Assert v0.0.0-20250426205601-1b0e5ea6e7ac h1:B6iLK3nv2ubDfk5Ve9Z2sRPqpTgPWgsm7PyaWlwr3NY= -git.sharkk.net/Go/Assert v0.0.0-20250426205601-1b0e5ea6e7ac/go.mod h1:7AMVm0RCtLlQfWsnKs6h/IdSfzj52/o0nR03rCW68gM= diff --git a/parser/ast.go b/parser/ast.go index 21a9ae7..73b91c1 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -43,6 +43,55 @@ func (as *AssignStatement) String() string { return fmt.Sprintf("%s = %s", as.Name.String(), as.Value.String()) } +// ElseIfClause represents an elseif condition +type ElseIfClause struct { + Condition Expression + Body []Statement +} + +func (eic *ElseIfClause) String() string { + var body string + for _, stmt := range eic.Body { + body += "\t" + stmt.String() + "\n" + } + return fmt.Sprintf("elseif %s then\n%s", eic.Condition.String(), body) +} + +// IfStatement represents conditional statements +type IfStatement struct { + Condition Expression + Body []Statement + ElseIfs []ElseIfClause + Else []Statement +} + +func (is *IfStatement) statementNode() {} +func (is *IfStatement) String() string { + var result string + + // If clause + result += fmt.Sprintf("if %s then\n", is.Condition.String()) + for _, stmt := range is.Body { + result += "\t" + stmt.String() + "\n" + } + + // ElseIf clauses + for _, elseif := range is.ElseIfs { + result += elseif.String() + } + + // Else clause + if len(is.Else) > 0 { + result += "else\n" + for _, stmt := range is.Else { + result += "\t" + stmt.String() + "\n" + } + } + + result += "end" + return result +} + // Identifier represents identifiers type Identifier struct { Value string diff --git a/parser/parser.go b/parser/parser.go index 9e3d003..d268c5d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -103,6 +103,8 @@ func (p *Parser) parseStatement() Statement { } p.addError("unexpected identifier, expected assignment or declaration") return nil + case IF: + return p.parseIfStatement() case ASSIGN: p.addError("assignment operator '=' without left-hand side identifier") return nil @@ -134,7 +136,7 @@ func (p *Parser) parseAssignStatement() *AssignStatement { p.nextToken() - stmt.Value = p.parseExpression(LOWEST) + stmt.Value = p.ParseExpression(LOWEST) if stmt.Value == nil { p.addError("expected expression after assignment operator") return nil @@ -143,8 +145,82 @@ func (p *Parser) parseAssignStatement() *AssignStatement { return stmt } -// parseExpression parses expressions using Pratt parsing -func (p *Parser) parseExpression(precedence Precedence) Expression { +// parseIfStatement parses if/elseif/else/end statements +func (p *Parser) parseIfStatement() *IfStatement { + stmt := &IfStatement{} + + p.nextToken() // move past 'if' + + stmt.Condition = p.ParseExpression(LOWEST) + if stmt.Condition == nil { + p.addError("expected condition after 'if'") + return nil + } + + p.nextToken() // move past condition + + // Parse if body + stmt.Body = p.parseBlockStatements(ELSEIF, ELSE, END) + + // Parse elseif clauses + for p.curTokenIs(ELSEIF) { + elseif := ElseIfClause{} + + p.nextToken() // move past 'elseif' + + elseif.Condition = p.ParseExpression(LOWEST) + if elseif.Condition == nil { + p.addError("expected condition after 'elseif'") + return nil + } + + p.nextToken() // move past condition + + elseif.Body = p.parseBlockStatements(ELSEIF, ELSE, END) + stmt.ElseIfs = append(stmt.ElseIfs, elseif) + } + + // Parse else clause + if p.curTokenIs(ELSE) { + p.nextToken() // move past 'else' + stmt.Else = p.parseBlockStatements(END) + } + + if !p.curTokenIs(END) { + p.addError("expected 'end' to close if statement") + return nil + } + + return stmt +} + +// parseBlockStatements parses statements until one of the terminator tokens +func (p *Parser) parseBlockStatements(terminators ...TokenType) []Statement { + statements := []Statement{} + + for !p.curTokenIs(EOF) && !p.isTerminator(terminators...) { + stmt := p.parseStatement() + if stmt != nil { + statements = append(statements, stmt) + } + p.nextToken() + } + + return statements +} + +// isTerminator checks if current token is one of the terminators +func (p *Parser) isTerminator(terminators ...TokenType) bool { + for _, terminator := range terminators { + if p.curTokenIs(terminator) { + return true + } + } + return false +} + +// ParseExpression parses expressions using Pratt parsing +func (p *Parser) ParseExpression(precedence Precedence) Expression { prefix := p.prefixParseFns[p.curToken.Type] if prefix == nil { p.noPrefixParseFnError(p.curToken.Type) @@ -254,7 +330,7 @@ func (p *Parser) parseNilLiteral() Expression { func (p *Parser) parseGroupedExpression() Expression { p.nextToken() - exp := p.parseExpression(LOWEST) + exp := p.ParseExpression(LOWEST) if exp == nil { return nil } @@ -302,10 +378,10 @@ func (p *Parser) parseTableLiteral() Expression { return nil } - pair.Value = p.parseExpression(LOWEST) + pair.Value = p.ParseExpression(LOWEST) } else { // Array-style element - pair.Value = p.parseExpression(LOWEST) + pair.Value = p.ParseExpression(LOWEST) } if pair.Value == nil { @@ -348,7 +424,7 @@ func (p *Parser) parseInfixExpression(left Expression) Expression { precedence := p.curPrecedence() p.nextToken() - expression.Right = p.parseExpression(precedence) + expression.Right = p.ParseExpression(precedence) if expression.Right == nil { p.addError(fmt.Sprintf("expected expression after operator '%s'", expression.Operator)) @@ -485,6 +561,14 @@ func tokenTypeString(t TokenType) string { return "," case VAR: return "var" + case IF: + return "if" + case ELSEIF: + return "elseif" + case ELSE: + return "else" + case END: + return "end" case EOF: return "end of file" case ILLEGAL: diff --git a/parser/parser_test.go b/parser/parser_test.go deleted file mode 100644 index 087d03a..0000000 --- a/parser/parser_test.go +++ /dev/null @@ -1,1024 +0,0 @@ -package parser - -import ( - "strings" - "testing" -) - -func TestExtendedNumberLiterals(t *testing.T) { - tests := []struct { - input string - expected float64 - desc string - }{ - // Hexadecimal - {"0x10", 16.0, "lowercase hex"}, - {"0X10", 16.0, "uppercase hex"}, - {"0xff", 255.0, "hex with letters"}, - {"0XFF", 255.0, "hex with uppercase letters"}, - {"0x0", 0.0, "hex zero"}, - {"0xDEADBEEF", 3735928559.0, "large hex"}, - - // Binary - {"0b1010", 10.0, "lowercase binary"}, - {"0B1010", 10.0, "uppercase binary"}, - {"0b0", 0.0, "binary zero"}, - {"0b1", 1.0, "binary one"}, - {"0b11111111", 255.0, "8-bit binary"}, - - // Scientific notation - {"1e2", 100.0, "simple scientific"}, - {"1E2", 100.0, "uppercase E"}, - {"1.5e2", 150.0, "decimal with exponent"}, - {"2e-1", 0.2, "negative exponent"}, - {"1.23e+4", 12300.0, "positive exponent with +"}, - {"3.14159e0", 3.14159, "zero exponent"}, - {"1e10", 1e10, "large exponent"}, - - // Regular decimals (should still work) - {"42", 42.0, "integer"}, - {"3.14", 3.14, "decimal"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - expr := p.parseExpression(LOWEST) - checkParserErrors(t, p) - - testNumberLiteral(t, expr, tt.expected) - }) - } -} - -func TestExtendedNumberAssignments(t *testing.T) { - tests := []struct { - input string - identifier string - expected float64 - desc string - }{ - {"hex = 0xFF", "hex", 255.0, "hex assignment"}, - {"bin = 0b1111", "bin", 15.0, "binary assignment"}, - {"sci = 1.5e3", "sci", 1500.0, "scientific assignment"}, - {"large = 0xDEADBEEF", "large", 3735928559.0, "large hex"}, - {"small = 2e-5", "small", 0.00002, "small scientific"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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.identifier { - t.Errorf("expected identifier %s, got %s", tt.identifier, stmt.Name.Value) - } - - testNumberLiteral(t, stmt.Value, tt.expected) - }) - } -} - -func TestExtendedNumberExpressions(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {"0x10 + 0b1010", "(16.00 + 10.00)", "hex + binary"}, - {"1e2 * 0xFF", "(100.00 * 255.00)", "scientific * hex"}, - {"0b11 - 1e1", "(3.00 - 10.00)", "binary - scientific"}, - {"(0x10 + 0b10) * 1e1", "((16.00 + 2.00) * 10.00)", "mixed with precedence"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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 TestExtendedNumberErrors(t *testing.T) { - tests := []struct { - input string - expectedError string - desc string - }{ - {"0x", "could not parse '0x' as hexadecimal number", "incomplete hex"}, - {"0b", "could not parse '0b' as binary number", "incomplete binary"}, - {"0xGHI", "could not parse '0xGHI' as hexadecimal number", "invalid hex digits"}, - {"0b123", "could not parse '0b123' as binary number", "invalid binary digits"}, - {"1e", "could not parse '1e' as number", "incomplete scientific"}, - {"1e+", "could not parse '1e+' as number", "scientific without digits"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - p.parseExpression(LOWEST) - - 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 - 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 TestExtendedNumberStringRepresentation(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {"0xFF", "255.00", "hex string representation"}, - {"0b1111", "15.00", "binary string representation"}, - {"1e3", "1000.00", "scientific string representation"}, - {"1.5e2", "150.00", "decimal scientific string representation"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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 TestTableWithExtendedNumbers(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {"{0xFF, 0b1010}", "{255.00, 10.00}", "array with hex and binary"}, - {"{hex = 0xFF, bin = 0b1010}", "{hex = 255.00, bin = 10.00}", "hash with extended numbers"}, - {"{1e2, 0x10, 0b10}", "{100.00, 16.00, 2.00}", "mixed number formats"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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 TestLexerExtendedNumbers(t *testing.T) { - tests := []struct { - input string - expected []Token - desc string - }{ - { - "0xFF + 0b1010", - []Token{ - {Type: NUMBER, Literal: "0xFF"}, - {Type: PLUS, Literal: "+"}, - {Type: NUMBER, Literal: "0b1010"}, - {Type: EOF, Literal: ""}, - }, - "hex and binary tokens", - }, - { - "1.5e-3 * 2E+4", - []Token{ - {Type: NUMBER, Literal: "1.5e-3"}, - {Type: STAR, Literal: "*"}, - {Type: NUMBER, Literal: "2E+4"}, - {Type: EOF, Literal: ""}, - }, - "scientific notation tokens", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - - for i, expectedToken := range tt.expected { - tok := l.NextToken() - if tok.Type != expectedToken.Type { - t.Errorf("token %d: expected type %v, got %v", i, expectedToken.Type, tok.Type) - } - if tok.Literal != expectedToken.Literal { - t.Errorf("token %d: expected literal %s, got %s", i, expectedToken.Literal, tok.Literal) - } - } - }) - } -} - -// Additional existing tests would remain unchanged... -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) { - 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 TestTableLiterals(t *testing.T) { - tests := []struct { - input string - expectedPairs int - isArray bool - description string - }{ - {"{}", 0, true, "empty table"}, - {"{1, 2, 3}", 3, true, "array-like table"}, - {"{a = 1, b = 2}", 2, false, "hash-like table"}, - {`{"hello", "world"}`, 2, true, "string array"}, - {"{x = true, y = false}", 2, false, "boolean hash"}, - {"{1}", 1, true, "single element array"}, - {"{key = nil}", 1, false, "nil value hash"}, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - expr := p.parseExpression(LOWEST) - checkParserErrors(t, p) - - table, ok := expr.(*TableLiteral) - if !ok { - t.Fatalf("expected TableLiteral, got %T", expr) - } - - if len(table.Pairs) != tt.expectedPairs { - t.Errorf("expected %d pairs, got %d", tt.expectedPairs, len(table.Pairs)) - } - - if table.IsArray() != tt.isArray { - t.Errorf("expected IsArray() = %t, got %t", tt.isArray, table.IsArray()) - } - }) - } -} - -func TestTableAssignments(t *testing.T) { - tests := []struct { - input string - identifier string - pairCount int - isArray bool - description string - }{ - {"arr = {1, 2, 3}", "arr", 3, true, "array assignment"}, - {"hash = {x = 1, y = 2}", "hash", 2, false, "hash assignment"}, - {"empty = {}", "empty", 0, true, "empty table assignment"}, - } - - for _, tt := range tests { - t.Run(tt.description, 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.identifier { - t.Errorf("expected identifier %s, got %s", tt.identifier, stmt.Name.Value) - } - - table, ok := stmt.Value.(*TableLiteral) - if !ok { - t.Fatalf("expected TableLiteral, got %T", stmt.Value) - } - - if len(table.Pairs) != tt.pairCount { - t.Errorf("expected %d pairs, got %d", tt.pairCount, len(table.Pairs)) - } - - if table.IsArray() != tt.isArray { - t.Errorf("expected IsArray() = %t, got %t", tt.isArray, table.IsArray()) - } - }) - } -} - -func TestTableStringRepresentation(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"{}", "{}"}, - {"{1, 2}", "{1.00, 2.00}"}, - {"{x = 1}", "{x = 1.00}"}, - {"{a = 1, b = 2}", "{a = 1.00, b = 2.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 TestAssignStatements(t *testing.T) { - tests := []struct { - input string - expectedIdentifier string - expectedValue any - isExpression bool - }{ - {"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 { - if stmt.Value.String() != tt.expectedValue.(string) { - t.Errorf("expected expression %s, got %s", tt.expectedValue.(string), stmt.Value.String()) - } - } else { - 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}, - {"{1, 2", "expected next token to be }", 1, 6}, - {"{a =", "expected expression after assignment operator", 1, 4}, - {"{a = 1,", "expected next token to be }", 1, 8}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - - switch tt.input { - case "(1 + 2", "+ 5", "1 +", "{1, 2", "{a =", "{a = 1,": - p.parseExpression(LOWEST) - default: - 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() - - 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") - } - - 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() -} - -func TestMultilineStringLiterals(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {`[[hello world]]`, "hello world", "basic multiline string"}, - {`[[]]`, "", "empty multiline string"}, - {`[[hello -world]]`, "hello\nworld", "multiline with newline"}, - {`[[hello [brackets] world]]`, "hello [brackets] world", "nested single brackets"}, - {`[[line1 -line2 -line3]]`, "line1\nline2\nline3", "multiple lines"}, - {`[[ tab and spaces ]]`, "\ttab and spaces\t", "tabs and spaces"}, - {`[[special chars: @#$%^&*()]]`, "special chars: @#$%^&*()", "special characters"}, - {`[["quotes" and 'apostrophes']]`, `"quotes" and 'apostrophes'`, "quotes inside multiline"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - expr := p.parseExpression(LOWEST) - checkParserErrors(t, p) - - testStringLiteral(t, expr, tt.expected) - }) - } -} - -func TestMultilineStringAssignments(t *testing.T) { - tests := []struct { - input string - identifier string - expected string - desc string - }{ - {`text = [[hello world]]`, "text", "hello world", "basic assignment"}, - {`empty = [[]]`, "empty", "", "empty multiline assignment"}, - {`multiline = [[line1 -line2]]`, "multiline", "line1\nline2", "multiline content assignment"}, - {`sql = [[SELECT * FROM users WHERE id = 1]]`, "sql", "SELECT * FROM users WHERE id = 1", "SQL query assignment"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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.identifier { - t.Errorf("expected identifier %s, got %s", tt.identifier, stmt.Name.Value) - } - - testStringLiteral(t, stmt.Value, tt.expected) - }) - } -} - -func TestMultilineStringInTables(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {`{[[hello]], [[world]]}`, `{"hello", "world"}`, "multiline strings in array"}, - {`{msg = [[hello world]]}`, `{msg = "hello world"}`, "multiline string in hash"}, - {`{[[key1]], "value1", key2 = [[value2]]}`, `{"key1", "value1", key2 = "value2"}`, "mixed strings in table"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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 TestMultilineStringExpressions(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {`[[hello]] + [[world]]`, `("hello" + "world")`, "multiline string concatenation"}, - {`([[hello]])`, `"hello"`, "parenthesized multiline string"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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 TestLexerMultilineStringTokens(t *testing.T) { - tests := []struct { - input string - expected []Token - desc string - }{ - { - `[[hello]] = [[world]]`, - []Token{ - {Type: STRING, Literal: "hello"}, - {Type: ASSIGN, Literal: "="}, - {Type: STRING, Literal: "world"}, - {Type: EOF, Literal: ""}, - }, - "multiline string tokens", - }, - { - `x = [[multiline -content]]`, - []Token{ - {Type: IDENT, Literal: "x"}, - {Type: ASSIGN, Literal: "="}, - {Type: STRING, Literal: "multiline\ncontent"}, - {Type: EOF, Literal: ""}, - }, - "multiline with newline tokens", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - - for i, expectedToken := range tt.expected { - tok := l.NextToken() - if tok.Type != expectedToken.Type { - t.Errorf("token %d: expected type %v, got %v", i, expectedToken.Type, tok.Type) - } - if tok.Literal != expectedToken.Literal { - t.Errorf("token %d: expected literal %q, got %q", i, expectedToken.Literal, tok.Literal) - } - } - }) - } -} - -func TestMultilineStringEdgeCases(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {`[[]]`, "", "empty multiline string"}, - {`[[a]]`, "a", "single character"}, - {`[[[]]]`, "[", "single brackets inside - first ]] is closing"}, - {`[[[nested]]]`, "[nested", "nested brackets - first ]] is closing"}, - {`[[]end]]`, "]end", "closing bracket in content"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - expr := p.parseExpression(LOWEST) - checkParserErrors(t, p) - - testStringLiteral(t, expr, tt.expected) - }) - } -} - -func TestMultilineStringErrors(t *testing.T) { - tests := []struct { - input string - expectedError string - desc string - }{ - {`[hello`, "unexpected token", "single bracket"}, - {`[[hello`, "", "unclosed multiline string - no error expected"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - l := NewLexer(tt.input) - p := NewParser(l) - p.ParseProgram() - - errors := p.Errors() - if len(errors) == 0 && tt.expectedError != "" { - t.Fatalf("expected parsing errors, got none") - } - - if tt.expectedError != "" { - found := false - for _, err := range errors { - if strings.Contains(err.Message, tt.expectedError) { - found = true - 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 TestMixedStringTypes(t *testing.T) { - tests := []struct { - input string - expected string - desc string - }{ - {`"regular" + [[multiline]]`, `("regular" + "multiline")`, "regular + multiline"}, - {`{[[key1]] = "value1", "key2" = [[value2]]}`, `{"key1" = "value1", "key2" = "value2"}`, "mixed in table"}, - } - - for _, tt := range tests { - t.Run(tt.desc, 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()) - } - }) - } -} diff --git a/parser/tests/assignments_test.go b/parser/tests/assignments_test.go new file mode 100644 index 0000000..88b7bbc --- /dev/null +++ b/parser/tests/assignments_test.go @@ -0,0 +1,110 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +func TestAssignStatements(t *testing.T) { + tests := []struct { + input string + expectedIdentifier string + expectedValue any + isExpression bool + }{ + {"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 := parser.NewLexer(tt.input) + p := parser.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].(*parser.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 { + if stmt.Value.String() != tt.expectedValue.(string) { + t.Errorf("expected expression %s, got %s", tt.expectedValue.(string), stmt.Value.String()) + } + } else { + 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 TestTableAssignments(t *testing.T) { + tests := []struct { + input string + identifier string + pairCount int + isArray bool + description string + }{ + {"arr = {1, 2, 3}", "arr", 3, true, "array assignment"}, + {"hash = {x = 1, y = 2}", "hash", 2, false, "hash assignment"}, + {"empty = {}", "empty", 0, true, "empty table assignment"}, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.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].(*parser.AssignStatement) + if !ok { + t.Fatalf("expected AssignStatement, got %T", program.Statements[0]) + } + + if stmt.Name.Value != tt.identifier { + t.Errorf("expected identifier %s, got %s", tt.identifier, stmt.Name.Value) + } + + table, ok := stmt.Value.(*parser.TableLiteral) + if !ok { + t.Fatalf("expected TableLiteral, got %T", stmt.Value) + } + + if len(table.Pairs) != tt.pairCount { + t.Errorf("expected %d pairs, got %d", tt.pairCount, len(table.Pairs)) + } + + if table.IsArray() != tt.isArray { + t.Errorf("expected IsArray() = %t, got %t", tt.isArray, table.IsArray()) + } + }) + } +} diff --git a/parser/tests/conditionals_test.go b/parser/tests/conditionals_test.go new file mode 100644 index 0000000..76196ca --- /dev/null +++ b/parser/tests/conditionals_test.go @@ -0,0 +1,250 @@ +package parser_test + +import ( + "strings" + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +func TestBasicIfStatement(t *testing.T) { + input := `if true then +x = 1 +end` + + l := parser.NewLexer(input) + p := parser.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].(*parser.IfStatement) + if !ok { + t.Fatalf("expected IfStatement, got %T", program.Statements[0]) + } + + testBooleanLiteral(t, stmt.Condition, true) + + if len(stmt.Body) != 1 { + t.Fatalf("expected 1 body statement, got %d", len(stmt.Body)) + } + + bodyStmt, ok := stmt.Body[0].(*parser.AssignStatement) + if !ok { + t.Fatalf("expected AssignStatement in body, got %T", stmt.Body[0]) + } + + if bodyStmt.Name.Value != "x" { + t.Errorf("expected identifier 'x', got %s", bodyStmt.Name.Value) + } +} + +func TestIfElseStatement(t *testing.T) { + input := `if false then +x = 1 +else +x = 2 +end` + + l := parser.NewLexer(input) + p := parser.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].(*parser.IfStatement) + if !ok { + t.Fatalf("expected IfStatement, got %T", program.Statements[0]) + } + + testBooleanLiteral(t, stmt.Condition, false) + + if len(stmt.Body) != 1 { + t.Fatalf("expected 1 body statement, got %d", len(stmt.Body)) + } + + if len(stmt.Else) != 1 { + t.Fatalf("expected 1 else statement, got %d", len(stmt.Else)) + } + + elseStmt, ok := stmt.Else[0].(*parser.AssignStatement) + if !ok { + t.Fatalf("expected AssignStatement in else, got %T", stmt.Else[0]) + } + + if elseStmt.Name.Value != "x" { + t.Errorf("expected identifier 'x', got %s", elseStmt.Name.Value) + } +} + +func TestIfElseIfElseStatement(t *testing.T) { + input := `if x then +a = 1 +elseif y then +a = 2 +elseif z then +a = 3 +else +a = 4 +end` + + l := parser.NewLexer(input) + p := parser.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].(*parser.IfStatement) + if !ok { + t.Fatalf("expected IfStatement, got %T", program.Statements[0]) + } + + testIdentifier(t, stmt.Condition, "x") + + if len(stmt.ElseIfs) != 2 { + t.Fatalf("expected 2 elseif clauses, got %d", len(stmt.ElseIfs)) + } + + testIdentifier(t, stmt.ElseIfs[0].Condition, "y") + testIdentifier(t, stmt.ElseIfs[1].Condition, "z") + + if len(stmt.Else) != 1 { + t.Fatalf("expected 1 else statement, got %d", len(stmt.Else)) + } +} + +func TestConditionalExpressions(t *testing.T) { + input := `if 1 + 2 then +x = 3 * 4 +end` + + l := parser.NewLexer(input) + p := parser.NewParser(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*parser.IfStatement) + + // Test condition is an infix expression + infix, ok := stmt.Condition.(*parser.InfixExpression) + if !ok { + t.Fatalf("expected InfixExpression condition, got %T", stmt.Condition) + } + + if infix.Operator != "+" { + t.Errorf("expected '+' operator, got %s", infix.Operator) + } + + // Test body has expression assignment + bodyStmt := stmt.Body[0].(*parser.AssignStatement) + bodyInfix, ok := bodyStmt.Value.(*parser.InfixExpression) + if !ok { + t.Fatalf("expected InfixExpression value, got %T", bodyStmt.Value) + } + + if bodyInfix.Operator != "*" { + t.Errorf("expected '*' operator, got %s", bodyInfix.Operator) + } +} + +func TestConditionalErrors(t *testing.T) { + tests := []struct { + input string + expectedError string + desc string + }{ + {"if then end", "expected condition after 'if'", "missing condition"}, + {"if true end", "expected 'end' to close if statement", "missing body"}, + {"if true then x = 1", "expected 'end' to close if statement", "missing end"}, + {"elseif true then end", "unexpected token 'elseif'", "elseif without if"}, + {"if true then x = 1 elseif then end", "expected condition after 'elseif'", "missing elseif condition"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + 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 + 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 TestConditionalStringRepresentation(t *testing.T) { + tests := []struct { + input string + contains []string + desc string + }{ + { + `if true then +x = 1 +end`, + []string{"if true then", "x = 1", "end"}, + "basic if statement", + }, + { + `if x then +a = 1 +else +a = 2 +end`, + []string{"if x then", "a = 1", "else", "a = 2", "end"}, + "if else statement", + }, + { + `if x then +a = 1 +elseif y then +a = 2 +end`, + []string{"if x then", "a = 1", "elseif y then", "a = 2", "end"}, + "if elseif statement", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + programStr := program.String() + for _, contain := range tt.contains { + if !strings.Contains(programStr, contain) { + t.Errorf("expected string representation to contain %q, got:\n%s", contain, programStr) + } + } + }) + } +} diff --git a/parser/tests/errors_test.go b/parser/tests/errors_test.go new file mode 100644 index 0000000..84f36ce --- /dev/null +++ b/parser/tests/errors_test.go @@ -0,0 +1,140 @@ +package parser_test + +import ( + "strings" + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +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}, + {"{1, 2", "expected next token to be }", 1, 6}, + {"{a =", "expected expression after assignment operator", 1, 4}, + {"{a = 1,", "expected next token to be }", 1, 8}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + + switch tt.input { + case "(1 + 2", "+ 5", "1 +", "{1, 2", "{a =", "{a = 1,": + p.ParseExpression(parser.LOWEST) + default: + 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 := parser.NewLexer(input) + p := parser.NewParser(l) + program := p.ParseProgram() + + 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") + } + + 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 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 := parser.NewLexer(tt.input) + p := parser.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) + } + }) + } +} diff --git a/parser/tests/expressions_test.go b/parser/tests/expressions_test.go new file mode 100644 index 0000000..0f4860f --- /dev/null +++ b/parser/tests/expressions_test.go @@ -0,0 +1,59 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +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 := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.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 := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + if expr.String() != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, expr.String()) + } + }) + } +} diff --git a/parser/tests/helpers_test.go b/parser/tests/helpers_test.go new file mode 100644 index 0000000..fac289f --- /dev/null +++ b/parser/tests/helpers_test.go @@ -0,0 +1,104 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +// Helper functions for testing specific node types +func testNumberLiteral(t *testing.T, expr parser.Expression, expected float64) { + t.Helper() + num, ok := expr.(*parser.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 parser.Expression, expected string) { + t.Helper() + str, ok := expr.(*parser.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 parser.Expression, expected bool) { + t.Helper() + boolean, ok := expr.(*parser.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 parser.Expression) { + t.Helper() + _, ok := expr.(*parser.NilLiteral) + if !ok { + t.Fatalf("expected NilLiteral, got %T", expr) + } +} + +func testIdentifier(t *testing.T, expr parser.Expression, expected string) { + t.Helper() + ident, ok := expr.(*parser.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 parser.Expression, left any, operator string, right any) { + t.Helper() + infix, ok := expr.(*parser.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.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() +} diff --git a/parser/tests/literals_test.go b/parser/tests/literals_test.go new file mode 100644 index 0000000..62440b7 --- /dev/null +++ b/parser/tests/literals_test.go @@ -0,0 +1,41 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +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) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.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) + } + }) + } +} diff --git a/parser/tests/parser_test.go b/parser/tests/parser_test.go new file mode 100644 index 0000000..6a61caf --- /dev/null +++ b/parser/tests/parser_test.go @@ -0,0 +1,203 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +func TestProgram(t *testing.T) { + input := `x = 42 +y = "hello" +z = true + false` + + l := parser.NewLexer(input) + p := parser.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].(*parser.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 TestMixedStatements(t *testing.T) { + input := `x = 42 +if x then + y = "hello" + if true then + z = {1, 2, 3} + end +end +arr = {a = 1, b = 2}` + + l := parser.NewLexer(input) + p := parser.NewParser(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 3 { + t.Fatalf("expected 3 statements, got %d", len(program.Statements)) + } + + // First statement: assignment + stmt1, ok := program.Statements[0].(*parser.AssignStatement) + if !ok { + t.Fatalf("statement 0: expected AssignStatement, got %T", program.Statements[0]) + } + if stmt1.Name.Value != "x" { + t.Errorf("expected identifier 'x', got %s", stmt1.Name.Value) + } + + // Second statement: if statement + stmt2, ok := program.Statements[1].(*parser.IfStatement) + if !ok { + t.Fatalf("statement 1: expected IfStatement, got %T", program.Statements[1]) + } + if len(stmt2.Body) != 2 { + t.Errorf("expected 2 body statements, got %d", len(stmt2.Body)) + } + + // Third statement: table assignment + stmt3, ok := program.Statements[2].(*parser.AssignStatement) + if !ok { + t.Fatalf("statement 2: expected AssignStatement, got %T", program.Statements[2]) + } + table, ok := stmt3.Value.(*parser.TableLiteral) + if !ok { + t.Fatalf("expected TableLiteral value, got %T", stmt3.Value) + } + if table.IsArray() { + t.Error("expected hash table, got array") + } +} + +func TestNestedConditionals(t *testing.T) { + input := `if a then + if b then + x = 1 + elseif c then + x = 2 + else + x = 3 + end + y = 4 +elseif d then + z = 5 +else + w = 6 +end` + + l := parser.NewLexer(input) + p := parser.NewParser(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("expected 1 statement, got %d", len(program.Statements)) + } + + outerIf, ok := program.Statements[0].(*parser.IfStatement) + if !ok { + t.Fatalf("expected IfStatement, got %T", program.Statements[0]) + } + + // Outer if should have 2 body statements: nested if and assignment + if len(outerIf.Body) != 2 { + t.Fatalf("expected 2 body statements, got %d", len(outerIf.Body)) + } + + // First body statement should be nested if + nestedIf, ok := outerIf.Body[0].(*parser.IfStatement) + if !ok { + t.Fatalf("expected nested IfStatement, got %T", outerIf.Body[0]) + } + + // Nested if should have 1 elseif and 1 else + if len(nestedIf.ElseIfs) != 1 { + t.Errorf("expected 1 elseif in nested if, got %d", len(nestedIf.ElseIfs)) + } + + if len(nestedIf.Else) != 1 { + t.Errorf("expected 1 else statement in nested if, got %d", len(nestedIf.Else)) + } + + // Outer if should have 1 elseif and 1 else + if len(outerIf.ElseIfs) != 1 { + t.Errorf("expected 1 elseif in outer if, got %d", len(outerIf.ElseIfs)) + } + + if len(outerIf.Else) != 1 { + t.Errorf("expected 1 else statement in outer if, got %d", len(outerIf.Else)) + } +} + +func TestComplexExpressions(t *testing.T) { + input := `result = (0xFF + 0b1010) * 1e2 +if result then + table = { + hex = 0xDEAD, + bin = 0b1111, + sci = 1.5e-3, + str = [[multiline +string]] + } +end` + + l := parser.NewLexer(input) + p := parser.NewParser(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 2 { + t.Fatalf("expected 2 statements, got %d", len(program.Statements)) + } + + // First statement: complex expression assignment + stmt1, ok := program.Statements[0].(*parser.AssignStatement) + if !ok { + t.Fatalf("expected AssignStatement, got %T", program.Statements[0]) + } + + // Should be a complex infix expression + _, ok = stmt1.Value.(*parser.InfixExpression) + if !ok { + t.Fatalf("expected InfixExpression, got %T", stmt1.Value) + } + + // Second statement: if with table assignment + stmt2, ok := program.Statements[1].(*parser.IfStatement) + if !ok { + t.Fatalf("expected IfStatement, got %T", program.Statements[1]) + } + + if len(stmt2.Body) != 1 { + t.Fatalf("expected 1 body statement, got %d", len(stmt2.Body)) + } + + bodyStmt, ok := stmt2.Body[0].(*parser.AssignStatement) + if !ok { + t.Fatalf("expected AssignStatement in body, got %T", stmt2.Body[0]) + } + + table, ok := bodyStmt.Value.(*parser.TableLiteral) + if !ok { + t.Fatalf("expected TableLiteral, got %T", bodyStmt.Value) + } + + if len(table.Pairs) != 4 { + t.Errorf("expected 4 table pairs, got %d", len(table.Pairs)) + } +} diff --git a/parser/tests/strings_test.go b/parser/tests/strings_test.go new file mode 100644 index 0000000..8351467 --- /dev/null +++ b/parser/tests/strings_test.go @@ -0,0 +1,87 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +func TestMultilineStringLiterals(t *testing.T) { + tests := []struct { + input string + expected string + desc string + }{ + {`[[hello world]]`, "hello world", "basic multiline string"}, + {`[[]]`, "", "empty multiline string"}, + {`[[hello +world]]`, "hello\nworld", "multiline with newline"}, + {`[[hello [brackets] world]]`, "hello [brackets] world", "nested single brackets"}, + {`[[line1 +line2 +line3]]`, "line1\nline2\nline3", "multiple lines"}, + {`[[ tab and spaces ]]`, "\ttab and spaces\t", "tabs and spaces"}, + {`[[special chars: @#$%^&*()]]`, "special chars: @#$%^&*()", "special characters"}, + {`[["quotes" and 'apostrophes']]`, `"quotes" and 'apostrophes'`, "quotes inside multiline"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + testStringLiteral(t, expr, tt.expected) + }) + } +} + +func TestMultilineStringInTables(t *testing.T) { + tests := []struct { + input string + expected string + desc string + }{ + {`{[[hello]], [[world]]}`, `{"hello", "world"}`, "multiline strings in array"}, + {`{msg = [[hello world]]}`, `{msg = "hello world"}`, "multiline string in hash"}, + {`{[[key1]], "value1", key2 = [[value2]]}`, `{"key1", "value1", key2 = "value2"}`, "mixed strings in table"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + if expr.String() != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, expr.String()) + } + }) + } +} + +func TestMixedStringTypes(t *testing.T) { + tests := []struct { + input string + expected string + desc string + }{ + {`"regular" + [[multiline]]`, `("regular" + "multiline")`, "regular + multiline"}, + {`{[[key1]] = "value1", "key2" = [[value2]]}`, `{"key1" = "value1", "key2" = "value2"}`, "mixed in table"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + if expr.String() != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, expr.String()) + } + }) + } +} diff --git a/parser/tests/tables_test.go b/parser/tests/tables_test.go new file mode 100644 index 0000000..0f8bb1e --- /dev/null +++ b/parser/tests/tables_test.go @@ -0,0 +1,96 @@ +package parser_test + +import ( + "testing" + + "git.sharkk.net/Sharkk/Mako/parser" +) + +func TestTableLiterals(t *testing.T) { + tests := []struct { + input string + expectedPairs int + isArray bool + description string + }{ + {"{}", 0, true, "empty table"}, + {"{1, 2, 3}", 3, true, "array-like table"}, + {"{a = 1, b = 2}", 2, false, "hash-like table"}, + {`{"hello", "world"}`, 2, true, "string array"}, + {"{x = true, y = false}", 2, false, "boolean hash"}, + {"{1}", 1, true, "single element array"}, + {"{key = nil}", 1, false, "nil value hash"}, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + table, ok := expr.(*parser.TableLiteral) + if !ok { + t.Fatalf("expected TableLiteral, got %T", expr) + } + + if len(table.Pairs) != tt.expectedPairs { + t.Errorf("expected %d pairs, got %d", tt.expectedPairs, len(table.Pairs)) + } + + if table.IsArray() != tt.isArray { + t.Errorf("expected IsArray() = %t, got %t", tt.isArray, table.IsArray()) + } + }) + } +} + +func TestTableStringRepresentation(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"{}", "{}"}, + {"{1, 2}", "{1.00, 2.00}"}, + {"{x = 1}", "{x = 1.00}"}, + {"{a = 1, b = 2}", "{a = 1.00, b = 2.00}"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + if expr.String() != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, expr.String()) + } + }) + } +} + +func TestTableWithExtendedNumbers(t *testing.T) { + tests := []struct { + input string + expected string + desc string + }{ + {"{0xFF, 0b1010}", "{255.00, 10.00}", "array with hex and binary"}, + {"{hex = 0xFF, bin = 0b1010}", "{hex = 255.00, bin = 10.00}", "hash with extended numbers"}, + {"{1e2, 0x10, 0b10}", "{100.00, 16.00, 2.00}", "mixed number formats"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + l := parser.NewLexer(tt.input) + p := parser.NewParser(l) + expr := p.ParseExpression(parser.LOWEST) + checkParserErrors(t, p) + + if expr.String() != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, expr.String()) + } + }) + } +} diff --git a/parser/token.go b/parser/token.go index af7f8bf..2caa96d 100644 --- a/parser/token.go +++ b/parser/token.go @@ -28,6 +28,10 @@ const ( // Keywords VAR + IF + ELSEIF + ELSE + END // Special EOF @@ -65,10 +69,14 @@ var precedences = map[TokenType]Precedence{ // lookupIdent checks if an identifier is a keyword func lookupIdent(ident string) TokenType { keywords := map[string]TokenType{ - "var": VAR, - "true": TRUE, - "false": FALSE, - "nil": NIL, + "var": VAR, + "true": TRUE, + "false": FALSE, + "nil": NIL, + "if": IF, + "elseif": ELSEIF, + "else": ELSE, + "end": END, } if tok, ok := keywords[ident]; ok {