import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) import pytest from tokenizer import tokenize, TokenType from parser import Parser, ParseError, NumberNode, BinOpNode, UnaryOpNode def parse(expr): """Helper: tokenize and parse in one step.""" return Parser(tokenize(expr)).parse() # ---------- Basic parsing ---------- def test_parse_number(): ast = parse("42") assert isinstance(ast, NumberNode) assert ast.value == 42.0 def test_parse_decimal(): ast = parse("3.14") assert isinstance(ast, NumberNode) assert ast.value == 3.14 def test_parse_addition(): ast = parse("3 + 4") assert isinstance(ast, BinOpNode) assert ast.op == TokenType.PLUS assert isinstance(ast.left, NumberNode) assert isinstance(ast.right, NumberNode) # ---------- Precedence ---------- def test_multiply_before_add(): """3 + 4 * 2 should parse as 3 + (4 * 2).""" ast = parse("3 + 4 * 2") assert ast.op == TokenType.PLUS assert isinstance(ast.right, BinOpNode) assert ast.right.op == TokenType.MULTIPLY def test_power_before_multiply(): """2 * 3 ^ 4 should parse as 2 * (3 ^ 4).""" ast = parse("2 * 3 ^ 4") assert ast.op == TokenType.MULTIPLY assert isinstance(ast.right, BinOpNode) assert ast.right.op == TokenType.POWER def test_parentheses_override_precedence(): """(3 + 4) * 2 should parse as (3 + 4) * 2.""" ast = parse("(3 + 4) * 2") assert ast.op == TokenType.MULTIPLY assert isinstance(ast.left, BinOpNode) assert ast.left.op == TokenType.PLUS # ---------- Associativity ---------- def test_left_associative_subtraction(): """10 - 3 - 2 should parse as (10 - 3) - 2.""" ast = parse("10 - 3 - 2") assert ast.op == TokenType.MINUS assert isinstance(ast.left, BinOpNode) assert ast.left.op == TokenType.MINUS assert isinstance(ast.right, NumberNode) def test_power_right_associative(): """2 ^ 3 ^ 4 should parse as 2 ^ (3 ^ 4).""" ast = parse("2 ^ 3 ^ 4") assert ast.op == TokenType.POWER assert isinstance(ast.left, NumberNode) assert isinstance(ast.right, BinOpNode) assert ast.right.op == TokenType.POWER # ---------- Unary minus ---------- def test_unary_minus(): ast = parse("-3") assert isinstance(ast, UnaryOpNode) assert ast.operand.value == 3.0 def test_double_negation(): ast = parse("--3") assert isinstance(ast, UnaryOpNode) assert isinstance(ast.operand, UnaryOpNode) assert ast.operand.operand.value == 3.0 def test_unary_minus_precedence(): """-3^2 should parse as -(3^2), not (-3)^2.""" ast = parse("-3 ^ 2") assert isinstance(ast, UnaryOpNode) assert isinstance(ast.operand, BinOpNode) assert ast.operand.op == TokenType.POWER def test_unary_minus_in_expression(): """2 * -3 should parse as 2 * (-(3)).""" ast = parse("2 * -3") assert ast.op == TokenType.MULTIPLY assert isinstance(ast.right, UnaryOpNode) # ---------- Nested parentheses ---------- def test_nested_parens(): ast = parse("((3))") assert isinstance(ast, NumberNode) assert ast.value == 3.0 def test_complex_nesting(): """((2 + 3) * (7 - 2))""" ast = parse("((2 + 3) * (7 - 2))") assert isinstance(ast, BinOpNode) assert ast.op == TokenType.MULTIPLY # ---------- Errors ---------- def test_missing_rparen(): with pytest.raises(ParseError): parse("(3 + 4") def test_empty_expression(): with pytest.raises(ParseError): parse("") def test_trailing_operator(): with pytest.raises(ParseError): parse("3 +") def test_empty_parens(): with pytest.raises(ParseError): parse("()")