mathlex 0.4.1

Mathematical expression parser for LaTeX and plain text notation, producing a language-agnostic AST
Documentation
//! Tests for precedence correctness in serialization.
//!
//! Verifies that Display and ToLatex produce output that parses back
//! with the correct precedence and associativity.

use mathlex::{parse, parse_latex, BinaryOp, ExprKind, Expression, ToLatex, UnaryOp};

// Helper to create a variable expression
fn var(name: &str) -> Expression {
    ExprKind::Variable(name.to_string()).into()
}

// Helper to create an integer expression
fn int(n: i64) -> Expression {
    ExprKind::Integer(n).into()
}

// =============================================================================
// Plain Text Precedence Tests
// =============================================================================

#[test]
fn test_unary_neg_of_sum() {
    // -(a + b) should serialize with parens and round-trip correctly
    let ast: Expression = ExprKind::Unary {
        op: UnaryOp::Neg,
        operand: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Add,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_string();
    assert!(s.contains("("), "Should have parens: {}", s);
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "-(a+b) should round-trip");
}

#[test]
fn test_unary_neg_of_product() {
    // -(a * b) should serialize with parens
    let ast: Expression = ExprKind::Unary {
        op: UnaryOp::Neg,
        operand: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Mul,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_string();
    assert!(s.contains("("), "Should have parens: {}", s);
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "-(a*b) should round-trip");
}

#[test]
fn test_power_left_associativity_needs_parens() {
    // (a^b)^c should serialize with parens (power is right-associative)
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Pow,
        left: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Pow,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
        right: Box::new(var("c")),
    }
    .into();
    let s = ast.to_string();
    assert!(s.contains("("), "Should have parens for (a^b)^c: {}", s);
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "(a^b)^c should round-trip");
}

#[test]
fn test_power_right_associativity_no_parens() {
    // a^(b^c) is natural right-associativity, no parens needed
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Pow,
        left: Box::new(var("a")),
        right: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Pow,
                left: Box::new(var("b")),
                right: Box::new(var("c")),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_string();
    // May or may not have parens, but should round-trip
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "a^(b^c) should round-trip");
}

#[test]
fn test_sum_in_product_needs_parens() {
    // (a + b) * c needs parens around the sum
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Mul,
        left: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Add,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
        right: Box::new(var("c")),
    }
    .into();
    let s = ast.to_string();
    assert!(s.contains("("), "Should have parens: {}", s);
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "(a+b)*c should round-trip");
}

#[test]
fn test_product_in_sum_no_parens() {
    // a * b + c doesn't need parens (mul has higher precedence)
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Add,
        left: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Mul,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
        right: Box::new(var("c")),
    }
    .into();
    let s = ast.to_string();
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "a*b+c should round-trip");
}

#[test]
fn test_subtraction_associativity() {
    // (a - b) - c vs a - (b - c)
    // Left: (a - b) - c = a - b - c (left associative, no parens needed)
    let left_assoc: Expression = ExprKind::Binary {
        op: BinaryOp::Sub,
        left: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Sub,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
        right: Box::new(var("c")),
    }
    .into();
    let s1 = left_assoc.to_string();
    let parsed1 = parse(&s1).unwrap();
    assert_eq!(left_assoc, parsed1, "(a-b)-c should round-trip");

    // Right: a - (b - c) needs explicit parens
    let right_assoc: Expression = ExprKind::Binary {
        op: BinaryOp::Sub,
        left: Box::new(var("a")),
        right: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Sub,
                left: Box::new(var("b")),
                right: Box::new(var("c")),
            }
            .into(),
        ),
    }
    .into();
    let s2 = right_assoc.to_string();
    assert!(s2.contains("("), "a-(b-c) should have parens: {}", s2);
    let parsed2 = parse(&s2).unwrap();
    assert_eq!(right_assoc, parsed2, "a-(b-c) should round-trip");
}

#[test]
fn test_division_associativity() {
    // Similar to subtraction - division is left associative
    // a / (b / c) needs explicit parens
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Div,
        left: Box::new(var("a")),
        right: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Div,
                left: Box::new(var("b")),
                right: Box::new(var("c")),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_string();
    assert!(s.contains("("), "a/(b/c) should have parens: {}", s);
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "a/(b/c) should round-trip");
}

#[test]
fn test_nested_unary() {
    // --a should round-trip
    let ast: Expression = ExprKind::Unary {
        op: UnaryOp::Neg,
        operand: Box::new(
            ExprKind::Unary {
                op: UnaryOp::Neg,
                operand: Box::new(var("a")),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_string();
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "--a should round-trip");
}

#[test]
fn test_complex_precedence_chain() {
    // a + b * c^d should parse correctly without parens
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Add,
        left: Box::new(var("a")),
        right: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Mul,
                left: Box::new(var("b")),
                right: Box::new(
                    ExprKind::Binary {
                        op: BinaryOp::Pow,
                        left: Box::new(var("c")),
                        right: Box::new(var("d")),
                    }
                    .into(),
                ),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_string();
    let parsed = parse(&s).unwrap();
    assert_eq!(ast, parsed, "a + b * c^d should round-trip");
}

// =============================================================================
// LaTeX Precedence Tests
// =============================================================================

#[test]
fn test_latex_unary_neg_of_sum() {
    let ast: Expression = ExprKind::Unary {
        op: UnaryOp::Neg,
        operand: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Add,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_latex();
    assert!(
        s.contains("(") || s.contains("\\left"),
        "Should have parens: {}",
        s
    );
    let parsed = parse_latex(&s).unwrap();
    assert_eq!(ast, parsed, "LaTeX -(a+b) should round-trip");
}

#[test]
fn test_latex_fraction_in_sum() {
    // a + (1/2) where 1/2 is a fraction
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Add,
        left: Box::new(var("a")),
        right: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Div,
                left: Box::new(int(1)),
                right: Box::new(int(2)),
            }
            .into(),
        ),
    }
    .into();
    let s = ast.to_latex();
    let parsed = parse_latex(&s).unwrap();
    assert_eq!(ast, parsed, "LaTeX a + frac should round-trip");
}

#[test]
fn test_latex_power_precedence() {
    // (a^b)^c needs parens
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Pow,
        left: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Pow,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
        right: Box::new(var("c")),
    }
    .into();
    let s = ast.to_latex();
    let parsed = parse_latex(&s).unwrap();
    assert_eq!(ast, parsed, "LaTeX (a^b)^c should round-trip");
}

#[test]
fn test_latex_mul_precedence() {
    // (a + b) \cdot c needs parens
    let ast: Expression = ExprKind::Binary {
        op: BinaryOp::Mul,
        left: Box::new(
            ExprKind::Binary {
                op: BinaryOp::Add,
                left: Box::new(var("a")),
                right: Box::new(var("b")),
            }
            .into(),
        ),
        right: Box::new(var("c")),
    }
    .into();
    let s = ast.to_latex();
    let parsed = parse_latex(&s).unwrap();
    assert_eq!(ast, parsed, "LaTeX (a+b)*c should round-trip");
}