diff --git a/parser/ast.go b/parser/ast.go index ab0477d..4486b97 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -305,7 +305,7 @@ func (ce *CallExpression) String() string { 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 { Operator string Right Expression @@ -313,6 +313,10 @@ type PrefixExpression struct { func (pe *PrefixExpression) expressionNode() {} 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()) } diff --git a/parser/parser.go b/parser/parser.go index f1538a1..b6e330a 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -55,6 +55,7 @@ func NewParser(lexer *Lexer) *Parser { p.registerPrefix(LPAREN, p.parseGroupedExpression) p.registerPrefix(LBRACE, p.parseTableLiteral) p.registerPrefix(MINUS, p.parsePrefixExpression) + p.registerPrefix(NOT, p.parsePrefixExpression) p.registerPrefix(FN, p.parseFunctionLiteral) p.infixParseFns = make(map[TokenType]func(Expression) Expression) @@ -68,6 +69,8 @@ func NewParser(lexer *Lexer) *Parser { p.registerInfix(GT, p.parseInfixExpression) p.registerInfix(LT_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(LBRACKET, p.parseIndexExpression) p.registerInfix(LPAREN, p.parseCallExpression) @@ -320,7 +323,7 @@ func (p *Parser) parseReturnStatement() *ReturnStatement { // canStartExpression checks if a token type can start an expression func (p *Parser) canStartExpression(tokenType TokenType) bool { 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 default: 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 func (p *Parser) isKeyword(t TokenType) bool { 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 default: return false @@ -1123,6 +1126,12 @@ func tokenTypeString(t TokenType) string { return "<=" case GT_EQ: return ">=" + case AND: + return "and" + case OR: + return "or" + case NOT: + return "not" case LPAREN: return "(" case RPAREN: diff --git a/parser/tests/expressions_test.go b/parser/tests/expressions_test.go index b5d1e2a..7fd300f 100644 --- a/parser/tests/expressions_test.go +++ b/parser/tests/expressions_test.go @@ -16,6 +16,10 @@ func TestPrefixExpressions(t *testing.T) { {"-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 { @@ -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) { tests := []struct { input string @@ -155,6 +209,25 @@ func TestOperatorPrecedence(t *testing.T) { {"-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)"}, @@ -172,10 +245,14 @@ func TestOperatorPrecedence(t *testing.T) { {"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 { @@ -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) { tests := []struct { input string @@ -201,7 +312,7 @@ func TestComplexExpressionsWithComparisons(t *testing.T) { {"table.count > arr[0] + 5", "member access comparison"}, {"-value <= max", "prefix 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 { @@ -209,12 +320,6 @@ func TestComplexExpressionsWithComparisons(t *testing.T) { l := parser.NewLexer(tt.input) p := parser.NewParser(l) 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) if expr == nil { diff --git a/parser/token.go b/parser/token.go index ae808dc..afb33e1 100644 --- a/parser/token.go +++ b/parser/token.go @@ -28,6 +28,11 @@ const ( LT_EQ // <= GT_EQ // >= + // Logical operators + AND // and + OR // or + NOT // not + // Delimiters LPAREN // ( RPAREN // ) @@ -73,17 +78,21 @@ type Precedence int const ( _ Precedence = iota LOWEST + PREC_OR // or + PREC_AND // and EQUALS // ==, != LESSGREATER // >, <, >=, <= SUM // +, - PRODUCT // *, / - PREFIX // -x, !x + PREFIX // -x, not x MEMBER // table[key], table.key CALL // function() ) // precedences maps token types to their precedence levels var precedences = map[TokenType]Precedence{ + OR: PREC_OR, + AND: PREC_AND, EQ: EQUALS, NOT_EQ: EQUALS, LT: LESSGREATER, @@ -105,6 +114,9 @@ func lookupIdent(ident string) TokenType { "true": TRUE, "false": FALSE, "nil": NIL, + "and": AND, + "or": OR, + "not": NOT, "if": IF, "then": THEN, "elseif": ELSEIF,