remake tests
This commit is contained in:
parent
7c99792706
commit
8a8801f1c0
2
go.sum
2
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=
|
|
@ -43,6 +43,55 @@ func (as *AssignStatement) String() string {
|
|||||||
return fmt.Sprintf("%s = %s", as.Name.String(), as.Value.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
|
// Identifier represents identifiers
|
||||||
type Identifier struct {
|
type Identifier struct {
|
||||||
Value string
|
Value string
|
||||||
|
@ -103,6 +103,8 @@ func (p *Parser) parseStatement() Statement {
|
|||||||
}
|
}
|
||||||
p.addError("unexpected identifier, expected assignment or declaration")
|
p.addError("unexpected identifier, expected assignment or declaration")
|
||||||
return nil
|
return nil
|
||||||
|
case IF:
|
||||||
|
return p.parseIfStatement()
|
||||||
case ASSIGN:
|
case ASSIGN:
|
||||||
p.addError("assignment operator '=' without left-hand side identifier")
|
p.addError("assignment operator '=' without left-hand side identifier")
|
||||||
return nil
|
return nil
|
||||||
@ -134,7 +136,7 @@ func (p *Parser) parseAssignStatement() *AssignStatement {
|
|||||||
|
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
stmt.Value = p.parseExpression(LOWEST)
|
stmt.Value = p.ParseExpression(LOWEST)
|
||||||
if stmt.Value == nil {
|
if stmt.Value == nil {
|
||||||
p.addError("expected expression after assignment operator")
|
p.addError("expected expression after assignment operator")
|
||||||
return nil
|
return nil
|
||||||
@ -143,8 +145,82 @@ func (p *Parser) parseAssignStatement() *AssignStatement {
|
|||||||
return stmt
|
return stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseExpression parses expressions using Pratt parsing
|
// parseIfStatement parses if/elseif/else/end statements
|
||||||
func (p *Parser) parseExpression(precedence Precedence) Expression {
|
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]
|
prefix := p.prefixParseFns[p.curToken.Type]
|
||||||
if prefix == nil {
|
if prefix == nil {
|
||||||
p.noPrefixParseFnError(p.curToken.Type)
|
p.noPrefixParseFnError(p.curToken.Type)
|
||||||
@ -254,7 +330,7 @@ func (p *Parser) parseNilLiteral() Expression {
|
|||||||
func (p *Parser) parseGroupedExpression() Expression {
|
func (p *Parser) parseGroupedExpression() Expression {
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
exp := p.parseExpression(LOWEST)
|
exp := p.ParseExpression(LOWEST)
|
||||||
if exp == nil {
|
if exp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -302,10 +378,10 @@ func (p *Parser) parseTableLiteral() Expression {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.Value = p.parseExpression(LOWEST)
|
pair.Value = p.ParseExpression(LOWEST)
|
||||||
} else {
|
} else {
|
||||||
// Array-style element
|
// Array-style element
|
||||||
pair.Value = p.parseExpression(LOWEST)
|
pair.Value = p.ParseExpression(LOWEST)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pair.Value == nil {
|
if pair.Value == nil {
|
||||||
@ -348,7 +424,7 @@ func (p *Parser) parseInfixExpression(left Expression) Expression {
|
|||||||
|
|
||||||
precedence := p.curPrecedence()
|
precedence := p.curPrecedence()
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
expression.Right = p.parseExpression(precedence)
|
expression.Right = p.ParseExpression(precedence)
|
||||||
|
|
||||||
if expression.Right == nil {
|
if expression.Right == nil {
|
||||||
p.addError(fmt.Sprintf("expected expression after operator '%s'", expression.Operator))
|
p.addError(fmt.Sprintf("expected expression after operator '%s'", expression.Operator))
|
||||||
@ -485,6 +561,14 @@ func tokenTypeString(t TokenType) string {
|
|||||||
return ","
|
return ","
|
||||||
case VAR:
|
case VAR:
|
||||||
return "var"
|
return "var"
|
||||||
|
case IF:
|
||||||
|
return "if"
|
||||||
|
case ELSEIF:
|
||||||
|
return "elseif"
|
||||||
|
case ELSE:
|
||||||
|
return "else"
|
||||||
|
case END:
|
||||||
|
return "end"
|
||||||
case EOF:
|
case EOF:
|
||||||
return "end of file"
|
return "end of file"
|
||||||
case ILLEGAL:
|
case ILLEGAL:
|
||||||
|
File diff suppressed because it is too large
Load Diff
110
parser/tests/assignments_test.go
Normal file
110
parser/tests/assignments_test.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
250
parser/tests/conditionals_test.go
Normal file
250
parser/tests/conditionals_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
140
parser/tests/errors_test.go
Normal file
140
parser/tests/errors_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
59
parser/tests/expressions_test.go
Normal file
59
parser/tests/expressions_test.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
104
parser/tests/helpers_test.go
Normal file
104
parser/tests/helpers_test.go
Normal file
@ -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()
|
||||||
|
}
|
41
parser/tests/literals_test.go
Normal file
41
parser/tests/literals_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
203
parser/tests/parser_test.go
Normal file
203
parser/tests/parser_test.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
87
parser/tests/strings_test.go
Normal file
87
parser/tests/strings_test.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
96
parser/tests/tables_test.go
Normal file
96
parser/tests/tables_test.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,10 @@ const (
|
|||||||
|
|
||||||
// Keywords
|
// Keywords
|
||||||
VAR
|
VAR
|
||||||
|
IF
|
||||||
|
ELSEIF
|
||||||
|
ELSE
|
||||||
|
END
|
||||||
|
|
||||||
// Special
|
// Special
|
||||||
EOF
|
EOF
|
||||||
@ -69,6 +73,10 @@ func lookupIdent(ident string) TokenType {
|
|||||||
"true": TRUE,
|
"true": TRUE,
|
||||||
"false": FALSE,
|
"false": FALSE,
|
||||||
"nil": NIL,
|
"nil": NIL,
|
||||||
|
"if": IF,
|
||||||
|
"elseif": ELSEIF,
|
||||||
|
"else": ELSE,
|
||||||
|
"end": END,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tok, ok := keywords[ident]; ok {
|
if tok, ok := keywords[ident]; ok {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user