477 lines
12 KiB
Go
477 lines
12 KiB
Go
package parser_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.sharkk.net/Sharkk/Mako/parser"
|
|
)
|
|
|
|
func TestPrefixExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
operator string
|
|
value any
|
|
}{
|
|
{"-5", "-", 5.0},
|
|
{"-x", "-", "x"},
|
|
{"-true", "-", true},
|
|
{"-(1 + 2)", "-", "(1.00 + 2.00)"},
|
|
{"not true", "not", true},
|
|
{"not false", "not", false},
|
|
{"not x", "not", "x"},
|
|
{"not (a == b)", "not", "(a == b)"},
|
|
}
|
|
|
|
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)
|
|
|
|
prefix, ok := expr.(*parser.PrefixExpression)
|
|
if !ok {
|
|
t.Fatalf("expected PrefixExpression, got %T", expr)
|
|
}
|
|
|
|
if prefix.Operator != tt.operator {
|
|
t.Errorf("expected operator %s, got %s", tt.operator, prefix.Operator)
|
|
}
|
|
|
|
switch expected := tt.value.(type) {
|
|
case float64:
|
|
testNumberLiteral(t, prefix.Right, expected)
|
|
case string:
|
|
if expected == "x" {
|
|
testIdentifier(t, prefix.Right, expected)
|
|
} else {
|
|
// It's an expression string
|
|
if prefix.Right.String() != expected {
|
|
t.Errorf("expected %s, got %s", expected, prefix.Right.String())
|
|
}
|
|
}
|
|
case bool:
|
|
testBooleanLiteral(t, prefix.Right, expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogicalExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
leftValue any
|
|
operator string
|
|
rightValue any
|
|
}{
|
|
{"true and false", true, "and", false},
|
|
{"false or true", false, "or", true},
|
|
{"x and y", "x", "and", "y"},
|
|
{"a or b", "a", "or", "b"},
|
|
{"success and valid", "success", "and", "valid"},
|
|
{"error or fallback", "error", "or", "fallback"},
|
|
}
|
|
|
|
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)
|
|
|
|
infix, ok := expr.(*parser.InfixExpression)
|
|
if !ok {
|
|
t.Fatalf("expected InfixExpression, got %T", expr)
|
|
}
|
|
|
|
if infix.Operator != tt.operator {
|
|
t.Errorf("expected operator %s, got %s", tt.operator, infix.Operator)
|
|
}
|
|
|
|
// Test left operand
|
|
switch leftVal := tt.leftValue.(type) {
|
|
case string:
|
|
testIdentifier(t, infix.Left, leftVal)
|
|
case bool:
|
|
testBooleanLiteral(t, infix.Left, leftVal)
|
|
}
|
|
|
|
// Test right operand
|
|
switch rightVal := tt.rightValue.(type) {
|
|
case string:
|
|
testIdentifier(t, infix.Right, rightVal)
|
|
case bool:
|
|
testBooleanLiteral(t, infix.Right, rightVal)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComparisonExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
leftValue any
|
|
operator string
|
|
rightValue any
|
|
}{
|
|
{"1 == 1", 1.0, "==", 1.0},
|
|
{"1 != 2", 1.0, "!=", 2.0},
|
|
{"x < y", "x", "<", "y"},
|
|
{"a > b", "a", ">", "b"},
|
|
{"5 <= 10", 5.0, "<=", 10.0},
|
|
{"10 >= 5", 10.0, ">=", 5.0},
|
|
{"true == false", true, "==", false},
|
|
{"nil != x", nil, "!=", "x"},
|
|
}
|
|
|
|
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)
|
|
|
|
infix, ok := expr.(*parser.InfixExpression)
|
|
if !ok {
|
|
t.Fatalf("expected InfixExpression, got %T", expr)
|
|
}
|
|
|
|
if infix.Operator != tt.operator {
|
|
t.Errorf("expected operator %s, got %s", tt.operator, infix.Operator)
|
|
}
|
|
|
|
// Test left operand
|
|
switch leftVal := tt.leftValue.(type) {
|
|
case float64:
|
|
testNumberLiteral(t, infix.Left, leftVal)
|
|
case string:
|
|
testIdentifier(t, infix.Left, leftVal)
|
|
case bool:
|
|
testBooleanLiteral(t, infix.Left, leftVal)
|
|
case nil:
|
|
testNilLiteral(t, infix.Left)
|
|
}
|
|
|
|
// Test right operand
|
|
switch rightVal := tt.rightValue.(type) {
|
|
case float64:
|
|
testNumberLiteral(t, infix.Right, rightVal)
|
|
case string:
|
|
testIdentifier(t, infix.Right, rightVal)
|
|
case bool:
|
|
testBooleanLiteral(t, infix.Right, rightVal)
|
|
case nil:
|
|
testNilLiteral(t, infix.Right)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}{
|
|
// Arithmetic precedence
|
|
{"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)"},
|
|
|
|
// Prefix with arithmetic
|
|
{"-1 + 2", "((-1.00) + 2.00)"},
|
|
{"-(1 + 2)", "(-(1.00 + 2.00))"},
|
|
{"-x * 2", "((-x) * 2.00)"},
|
|
{"not true", "(not true)"},
|
|
{"not x", "(not x)"},
|
|
|
|
// Logical operator precedence
|
|
{"true or false and true", "(true or (false and true))"},
|
|
{"false and true or false", "((false and true) or false)"},
|
|
{"a or b and c", "(a or (b and c))"},
|
|
{"x and y or z", "((x and y) or z)"},
|
|
|
|
// Logical with comparison
|
|
{"a == b and c != d", "((a == b) and (c != d))"},
|
|
{"x < y or z > w", "((x < y) or (z > w))"},
|
|
{"not a == b", "((not a) == b)"},
|
|
{"not x and y", "((not x) and y)"},
|
|
|
|
// Logical with arithmetic
|
|
{"a + b and c * d", "((a + b) and (c * d))"},
|
|
{"x or y + z", "(x or (y + z))"},
|
|
{"not a + b", "((not a) + b)"},
|
|
|
|
// Comparison precedence
|
|
{"1 + 2 == 3", "((1.00 + 2.00) == 3.00)"},
|
|
{"1 * 2 < 3 + 4", "((1.00 * 2.00) < (3.00 + 4.00))"},
|
|
{"a + b != c * d", "((a + b) != (c * d))"},
|
|
{"x <= y + z", "(x <= (y + z))"},
|
|
{"a * b >= c / d", "((a * b) >= (c / d))"},
|
|
|
|
// Comparison chaining
|
|
{"a == b != c", "((a == b) != c)"},
|
|
{"x < y <= z", "((x < y) <= z)"},
|
|
|
|
// Member access with operators
|
|
{"table.key + 1", "(table.key + 1.00)"},
|
|
{"arr[0] * 2", "(arr[0.00] * 2.00)"},
|
|
{"obj.x == obj.y", "(obj.x == obj.y)"},
|
|
{"-table.value", "(-table.value)"},
|
|
{"not obj.active", "(not obj.active)"},
|
|
{"obj.x and obj.y", "(obj.x and obj.y)"},
|
|
|
|
// Complex combinations
|
|
{"-x + y * z == a.b", "(((-x) + (y * z)) == a.b)"},
|
|
{"(a + b) * c <= d[0]", "(((a + b) * c) <= d[0.00])"},
|
|
{"not success and attempts > 0 or fallback", "(((not success) and (attempts > 0.00)) or fallback)"},
|
|
{"a == b and c > d or e != f", "(((a == b) and (c > d)) or (e != f))"},
|
|
}
|
|
|
|
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 TestComplexLogicalExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
desc string
|
|
}{
|
|
{"not (a and b)", "negated grouped logical"},
|
|
{"a and b or c and d", "mixed logical operators"},
|
|
{"success and valid or error and retry", "complex boolean logic"},
|
|
{"not failed and attempts < max_attempts", "logical with comparison"},
|
|
{"(ready or waiting) and not cancelled", "grouped logical with negation"},
|
|
{"x > 0 and y > 0 and z > 0", "multiple and conditions"},
|
|
{"error or warning or info", "multiple or conditions"},
|
|
}
|
|
|
|
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 == nil {
|
|
t.Error("expected non-nil expression")
|
|
}
|
|
|
|
// Verify the expression can be converted to string (basic sanity check)
|
|
result := expr.String()
|
|
if result == "" {
|
|
t.Error("expected non-empty string representation")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComplexExpressionsWithComparisons(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
desc string
|
|
}{
|
|
{"x + 1 == y * 2", "arithmetic comparison"},
|
|
{"table.count > arr[0] + 5", "member access comparison"},
|
|
{"-value <= max", "prefix comparison"},
|
|
{"(a + b) != (c - d)", "grouped comparison"},
|
|
{"obj.x < obj.y and obj.y > obj.z", "multiple comparisons with logical"},
|
|
}
|
|
|
|
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 == nil {
|
|
t.Error("expected non-nil expression")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssignmentExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
targetName string
|
|
value any
|
|
isDeclaration bool
|
|
desc string
|
|
}{
|
|
{"(x = 5)", "x", 5.0, true, "simple assignment"},
|
|
{"(y = true)", "y", true, true, "boolean assignment"},
|
|
{"(name = \"test\")", "name", "test", true, "string assignment"},
|
|
{"(count = nil)", "count", nil, true, "nil assignment"},
|
|
{"(result = x + 1)", "result", "(x + 1.00)", true, "expression assignment"},
|
|
{"(flag = not active)", "flag", "(not active)", true, "prefix expression assignment"},
|
|
}
|
|
|
|
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)
|
|
|
|
assignExpr, ok := expr.(*parser.Assignment)
|
|
if !ok {
|
|
t.Fatalf("expected AssignExpression, got %T", expr)
|
|
}
|
|
|
|
// Test target name
|
|
ident, ok := assignExpr.Target.(*parser.Identifier)
|
|
if !ok {
|
|
t.Fatalf("expected Identifier for assignment target, got %T", assignExpr.Target)
|
|
}
|
|
|
|
if ident.Value != tt.targetName {
|
|
t.Errorf("expected target name %s, got %s", tt.targetName, ident.Value)
|
|
}
|
|
|
|
// Test assignment value
|
|
switch expected := tt.value.(type) {
|
|
case float64:
|
|
testNumberLiteral(t, assignExpr.Value, expected)
|
|
case string:
|
|
if expected == "test" {
|
|
testStringLiteral(t, assignExpr.Value, expected)
|
|
} else {
|
|
// It's an expression string
|
|
if assignExpr.Value.String() != expected {
|
|
t.Errorf("expected value %s, got %s", expected, assignExpr.Value.String())
|
|
}
|
|
}
|
|
case bool:
|
|
testBooleanLiteral(t, assignExpr.Value, expected)
|
|
case nil:
|
|
testNilLiteral(t, assignExpr.Value)
|
|
}
|
|
|
|
// Test declaration flag
|
|
if assignExpr.IsDeclaration != tt.isDeclaration {
|
|
t.Errorf("expected IsDeclaration %v, got %v", tt.isDeclaration, assignExpr.IsDeclaration)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssignmentExpressionWithComplexExpressions(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
desc string
|
|
}{
|
|
{"(result = func(a, b))", "function call assignment"},
|
|
{"(value = table[key])", "index expression assignment"},
|
|
{"(prop = obj.field)", "dot expression assignment"},
|
|
{"(sum = a + b * c)", "complex arithmetic assignment"},
|
|
{"(valid = x > 0 and y < 10)", "logical expression assignment"},
|
|
}
|
|
|
|
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)
|
|
|
|
assignExpr, ok := expr.(*parser.Assignment)
|
|
if !ok {
|
|
t.Fatalf("expected AssignExpression, got %T", expr)
|
|
}
|
|
|
|
if assignExpr.Target == nil {
|
|
t.Error("expected non-nil assignment target")
|
|
}
|
|
|
|
if assignExpr.Value == nil {
|
|
t.Error("expected non-nil assignment value")
|
|
}
|
|
|
|
// Verify string representation
|
|
result := assignExpr.String()
|
|
if result == "" {
|
|
t.Error("expected non-empty string representation")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssignmentExpressionErrors(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expectedErr string
|
|
desc string
|
|
}{
|
|
{"(x =)", "expected expression after assignment operator", "missing value"},
|
|
{"(= 5)", "unexpected assignment operator", "missing target"},
|
|
{"(x = )", "expected expression after assignment operator", "empty value"},
|
|
}
|
|
|
|
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)
|
|
|
|
if p.HasErrors() {
|
|
errors := p.ErrorStrings()
|
|
found := false
|
|
for _, err := range errors {
|
|
if containsSubstring(err, tt.expectedErr) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected error containing '%s', got %v", tt.expectedErr, errors)
|
|
}
|
|
} else {
|
|
t.Errorf("expected parse error for input '%s'", tt.input)
|
|
}
|
|
|
|
if expr != nil {
|
|
t.Errorf("expected nil expression for invalid input, got %T", expr)
|
|
}
|
|
})
|
|
}
|
|
}
|