logical operators

This commit is contained in:
Sky Johnson 2025-06-11 00:23:50 -05:00
parent 3ea58a55c9
commit c691c90c69
4 changed files with 141 additions and 11 deletions

View File

@ -305,7 +305,7 @@ func (ce *CallExpression) String() string {
return fmt.Sprintf("%s(%s)", ce.Function.String(), joinStrings(args, ", ")) return fmt.Sprintf("%s(%s)", ce.Function.String(), joinStrings(args, ", "))
} }
// PrefixExpression represents prefix operations like -x // PrefixExpression represents prefix operations like -x, not x
type PrefixExpression struct { type PrefixExpression struct {
Operator string Operator string
Right Expression Right Expression
@ -313,6 +313,10 @@ type PrefixExpression struct {
func (pe *PrefixExpression) expressionNode() {} func (pe *PrefixExpression) expressionNode() {}
func (pe *PrefixExpression) String() string { func (pe *PrefixExpression) String() string {
// Add space for word operators
if pe.Operator == "not" {
return fmt.Sprintf("(%s %s)", pe.Operator, pe.Right.String())
}
return fmt.Sprintf("(%s%s)", pe.Operator, pe.Right.String()) return fmt.Sprintf("(%s%s)", pe.Operator, pe.Right.String())
} }

View File

@ -55,6 +55,7 @@ func NewParser(lexer *Lexer) *Parser {
p.registerPrefix(LPAREN, p.parseGroupedExpression) p.registerPrefix(LPAREN, p.parseGroupedExpression)
p.registerPrefix(LBRACE, p.parseTableLiteral) p.registerPrefix(LBRACE, p.parseTableLiteral)
p.registerPrefix(MINUS, p.parsePrefixExpression) p.registerPrefix(MINUS, p.parsePrefixExpression)
p.registerPrefix(NOT, p.parsePrefixExpression)
p.registerPrefix(FN, p.parseFunctionLiteral) p.registerPrefix(FN, p.parseFunctionLiteral)
p.infixParseFns = make(map[TokenType]func(Expression) Expression) p.infixParseFns = make(map[TokenType]func(Expression) Expression)
@ -68,6 +69,8 @@ func NewParser(lexer *Lexer) *Parser {
p.registerInfix(GT, p.parseInfixExpression) p.registerInfix(GT, p.parseInfixExpression)
p.registerInfix(LT_EQ, p.parseInfixExpression) p.registerInfix(LT_EQ, p.parseInfixExpression)
p.registerInfix(GT_EQ, p.parseInfixExpression) p.registerInfix(GT_EQ, p.parseInfixExpression)
p.registerInfix(AND, p.parseInfixExpression)
p.registerInfix(OR, p.parseInfixExpression)
p.registerInfix(DOT, p.parseDotExpression) p.registerInfix(DOT, p.parseDotExpression)
p.registerInfix(LBRACKET, p.parseIndexExpression) p.registerInfix(LBRACKET, p.parseIndexExpression)
p.registerInfix(LPAREN, p.parseCallExpression) p.registerInfix(LPAREN, p.parseCallExpression)
@ -320,7 +323,7 @@ func (p *Parser) parseReturnStatement() *ReturnStatement {
// canStartExpression checks if a token type can start an expression // canStartExpression checks if a token type can start an expression
func (p *Parser) canStartExpression(tokenType TokenType) bool { func (p *Parser) canStartExpression(tokenType TokenType) bool {
switch tokenType { switch tokenType {
case IDENT, NUMBER, STRING, TRUE, FALSE, NIL, LPAREN, LBRACE, MINUS, FN: case IDENT, NUMBER, STRING, TRUE, FALSE, NIL, LPAREN, LBRACE, MINUS, NOT, FN:
return true return true
default: default:
return false return false
@ -1003,7 +1006,7 @@ func (p *Parser) expectPeekIdent() bool {
// isKeyword checks if a token type is a keyword that can be used as identifier // isKeyword checks if a token type is a keyword that can be used as identifier
func (p *Parser) isKeyword(t TokenType) bool { func (p *Parser) isKeyword(t TokenType) bool {
switch t { switch t {
case TRUE, FALSE, NIL, IF, THEN, ELSEIF, ELSE, END, ECHO, FOR, WHILE, IN, DO, BREAK, EXIT, FN, RETURN: case TRUE, FALSE, NIL, AND, OR, NOT, IF, THEN, ELSEIF, ELSE, END, ECHO, FOR, WHILE, IN, DO, BREAK, EXIT, FN, RETURN:
return true return true
default: default:
return false return false
@ -1123,6 +1126,12 @@ func tokenTypeString(t TokenType) string {
return "<=" return "<="
case GT_EQ: case GT_EQ:
return ">=" return ">="
case AND:
return "and"
case OR:
return "or"
case NOT:
return "not"
case LPAREN: case LPAREN:
return "(" return "("
case RPAREN: case RPAREN:

View File

@ -16,6 +16,10 @@ func TestPrefixExpressions(t *testing.T) {
{"-x", "-", "x"}, {"-x", "-", "x"},
{"-true", "-", true}, {"-true", "-", true},
{"-(1 + 2)", "-", "(1.00 + 2.00)"}, {"-(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 { for _, tt := range tests {
@ -53,6 +57,56 @@ func TestPrefixExpressions(t *testing.T) {
} }
} }
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) { func TestComparisonExpressions(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
@ -155,6 +209,25 @@ func TestOperatorPrecedence(t *testing.T) {
{"-1 + 2", "((-1.00) + 2.00)"}, {"-1 + 2", "((-1.00) + 2.00)"},
{"-(1 + 2)", "(-(1.00 + 2.00))"}, {"-(1 + 2)", "(-(1.00 + 2.00))"},
{"-x * 2", "((-x) * 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 // Comparison precedence
{"1 + 2 == 3", "((1.00 + 2.00) == 3.00)"}, {"1 + 2 == 3", "((1.00 + 2.00) == 3.00)"},
@ -172,10 +245,14 @@ func TestOperatorPrecedence(t *testing.T) {
{"arr[0] * 2", "(arr[0.00] * 2.00)"}, {"arr[0] * 2", "(arr[0.00] * 2.00)"},
{"obj.x == obj.y", "(obj.x == obj.y)"}, {"obj.x == obj.y", "(obj.x == obj.y)"},
{"-table.value", "(-table.value)"}, {"-table.value", "(-table.value)"},
{"not obj.active", "(not obj.active)"},
{"obj.x and obj.y", "(obj.x and obj.y)"},
// Complex combinations // Complex combinations
{"-x + y * z == a.b", "(((-x) + (y * z)) == a.b)"}, {"-x + y * z == a.b", "(((-x) + (y * z)) == a.b)"},
{"(a + b) * c <= d[0]", "(((a + b) * c) <= d[0.00])"}, {"(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 { for _, tt := range tests {
@ -192,6 +269,40 @@ func TestOperatorPrecedence(t *testing.T) {
} }
} }
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) { func TestComplexExpressionsWithComparisons(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
@ -201,7 +312,7 @@ func TestComplexExpressionsWithComparisons(t *testing.T) {
{"table.count > arr[0] + 5", "member access comparison"}, {"table.count > arr[0] + 5", "member access comparison"},
{"-value <= max", "prefix comparison"}, {"-value <= max", "prefix comparison"},
{"(a + b) != (c - d)", "grouped comparison"}, {"(a + b) != (c - d)", "grouped comparison"},
{"obj.x < obj.y && obj.y > obj.z", "multiple comparisons"}, // Note: && not implemented yet {"obj.x < obj.y and obj.y > obj.z", "multiple comparisons with logical"},
} }
for _, tt := range tests { for _, tt := range tests {
@ -209,12 +320,6 @@ func TestComplexExpressionsWithComparisons(t *testing.T) {
l := parser.NewLexer(tt.input) l := parser.NewLexer(tt.input)
p := parser.NewParser(l) p := parser.NewParser(l)
expr := p.ParseExpression(parser.LOWEST) expr := p.ParseExpression(parser.LOWEST)
// Skip && test since it's not implemented
if tt.input == "obj.x < obj.y && obj.y > obj.z" {
return
}
checkParserErrors(t, p) checkParserErrors(t, p)
if expr == nil { if expr == nil {

View File

@ -28,6 +28,11 @@ const (
LT_EQ // <= LT_EQ // <=
GT_EQ // >= GT_EQ // >=
// Logical operators
AND // and
OR // or
NOT // not
// Delimiters // Delimiters
LPAREN // ( LPAREN // (
RPAREN // ) RPAREN // )
@ -73,17 +78,21 @@ type Precedence int
const ( const (
_ Precedence = iota _ Precedence = iota
LOWEST LOWEST
PREC_OR // or
PREC_AND // and
EQUALS // ==, != EQUALS // ==, !=
LESSGREATER // >, <, >=, <= LESSGREATER // >, <, >=, <=
SUM // +, - SUM // +, -
PRODUCT // *, / PRODUCT // *, /
PREFIX // -x, !x PREFIX // -x, not x
MEMBER // table[key], table.key MEMBER // table[key], table.key
CALL // function() CALL // function()
) )
// precedences maps token types to their precedence levels // precedences maps token types to their precedence levels
var precedences = map[TokenType]Precedence{ var precedences = map[TokenType]Precedence{
OR: PREC_OR,
AND: PREC_AND,
EQ: EQUALS, EQ: EQUALS,
NOT_EQ: EQUALS, NOT_EQ: EQUALS,
LT: LESSGREATER, LT: LESSGREATER,
@ -105,6 +114,9 @@ func lookupIdent(ident string) TokenType {
"true": TRUE, "true": TRUE,
"false": FALSE, "false": FALSE,
"nil": NIL, "nil": NIL,
"and": AND,
"or": OR,
"not": NOT,
"if": IF, "if": IF,
"then": THEN, "then": THEN,
"elseif": ELSEIF, "elseif": ELSEIF,