From cb4ff431e490b143ddd704bbf6f0e34d3c85842c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:24:26 +0000 Subject: [PATCH 1/3] Initial plan From 4aec8efc4cf3d62bef20a662264053351fd17ebb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:34:19 +0000 Subject: [PATCH 2/3] Add expression parser example demonstrating enums, nested choices, and repetitions Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- derive/README.md | 142 ++++++ derive/examples/expression_parser.pest | 49 ++ derive/examples/expression_parser.rs | 626 +++++++++++++++++++++++++ 3 files changed, 817 insertions(+) create mode 100644 derive/examples/expression_parser.pest create mode 100644 derive/examples/expression_parser.rs diff --git a/derive/README.md b/derive/README.md index a7b7e27..49ad21b 100644 --- a/derive/README.md +++ b/derive/README.md @@ -369,3 +369,145 @@ The `default` attribute generates code that: 3. If parsing fails with other errors, propagates the error This provides a clean, type-safe way to handle optional grammar elements while keeping your AST representation simple and avoiding the complexity of `Option` handling. + +## Advanced Patterns: Enums, Nested Choices, and Repetitions + +For more complex grammars involving operators and expression parsing, pest-ast provides patterns for handling: + +1. **Parsing into enums** - representing grammar alternatives +2. **Nested choices** - patterns like `(plus | minus)` +3. **Repetitions of anonymous sequences** - patterns like `(operator ~ operand)*` + +### Parsing into Enums + +When your grammar has alternatives (using `|`), you can use enums to represent them. + +**Example Grammar:** +```pest +abc = { a | b | c } +a = { "a" } +b = { "b" } +c = { "c" } +``` + +**Rust AST:** +```rust +#[derive(FromPest)] +#[pest_ast(rule(Rule::a))] +struct A; + +#[derive(FromPest)] +#[pest_ast(rule(Rule::b))] +struct B; + +#[derive(FromPest)] +#[pest_ast(rule(Rule::c))] +struct C; + +// Enum uses the parent rule that contains the choice +#[derive(FromPest)] +#[pest_ast(rule(Rule::abc))] +enum Abc { + A(A), + B(B), + C(C), +} +``` + +See `derive/examples/simple_enum_derives.rs` for a complete example. + +### Nested Choices with Manual FromPest + +For patterns like `arith_expr = { term ~ ((plus | minus) ~ term)* }`, the `(plus | minus)` choice +doesn't have its own grammar rule. You can handle this by implementing `FromPest` manually: + +**Example Grammar:** +```pest +plus = { "+" } +minus = { "-" } +arith_expr = { term ~ ((plus | minus) ~ term)* } +``` + +**Rust AST:** +```rust +#[derive(FromPest)] +#[pest_ast(rule(Rule::plus))] +pub struct Plus; + +#[derive(FromPest)] +#[pest_ast(rule(Rule::minus))] +pub struct Minus; + +// Manual implementation tries each alternative +#[derive(Debug, Clone, PartialEq)] +pub enum AddOp { + Plus(Plus), + Minus(Minus), +} + +impl<'pest> FromPest<'pest> for AddOp { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + // Try Plus first + if let Ok(plus) = Plus::from_pest(pest) { + return Ok(AddOp::Plus(plus)); + } + // Try Minus + if let Ok(minus) = Minus::from_pest(pest) { + return Ok(AddOp::Minus(minus)); + } + Err(from_pest::ConversionError::NoMatch) + } +} +``` + +### Repetitions of Anonymous Sequences + +For grammar patterns like `term ~ ((operator ~ operand)*)`, the repeated `(operator ~ operand)` pairs +don't have their own grammar rule. Create a "tail" struct with manual `FromPest` implementation: + +**Example Grammar:** +```pest +term = { factor ~ ((mul | div) ~ factor)* } +``` + +**Rust AST:** +```rust +// One (operator, operand) pair from the repetition +#[derive(Debug)] +pub struct TermTail<'pest> { + pub op: MulOp, + pub factor: Factor<'pest>, +} + +impl<'pest> FromPest<'pest> for TermTail<'pest> { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + // First try to get an operator - if not present, no match + let op = MulOp::from_pest(pest)?; + // Then get the operand + let factor = Factor::from_pest(pest) + .map_err(|_| from_pest::ConversionError::NoMatch)?; + Ok(TermTail { op, factor }) + } +} + +// The main structure uses Vec for the repetition +#[derive(FromPest, Debug)] +#[pest_ast(rule(Rule::term))] +pub struct Term<'pest> { + pub first: Factor<'pest>, + pub rest: Vec>, // Handles the (op ~ operand)* part +} +``` + +For a complete working example demonstrating all these patterns with an expression parser, +see `derive/examples/expression_parser.rs` and `derive/examples/expression_parser.pest`. diff --git a/derive/examples/expression_parser.pest b/derive/examples/expression_parser.pest new file mode 100644 index 0000000..50507f8 --- /dev/null +++ b/derive/examples/expression_parser.pest @@ -0,0 +1,49 @@ +// Grammar demonstrating patterns for: +// 1. Repetition of anonymous sequences: comparison = { arith_expr ~ (comp_op ~ arith_expr)* } +// 2. Nested choices in repetitions: arith_expr = { term ~ ((plus|minus) ~ term)* } +// 3. Parsing into enums for operators + +WHITESPACE = _{ " " | "\t" | "\n" | "\r" } + +// Literals - using @ (atomic) to prevent WHITESPACE from being included in spans +number = @{ ASCII_DIGIT+ } +identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* } + +// Atomic expressions +atom = { number | identifier | "(" ~ expr ~ ")" } + +// Multiplicative operators (for nested choice example) +mul = { "*" } +div = { "/" } + +// Additive operators (for nested choice example) +plus = { "+" } +minus = { "-" } + +// Comparison operators (for nested choice example) +eq = { "==" } +neq = { "!=" } +lt = { "<" } +gt = { ">" } + +// Expressions with operator precedence +// Each level demonstrates the pattern: term ~ (operator ~ term)* + +// Factor: atom +factor = { atom } + +// Term: factor ~ ((mul | div) ~ factor)* +// This demonstrates nested choice (mul | div) in a repetition +term = { factor ~ ((mul | div) ~ factor)* } + +// Arithmetic expression: term ~ ((plus | minus) ~ term)* +// This demonstrates nested choice (plus | minus) in a repetition +arith_expr = { term ~ ((plus | minus) ~ term)* } + +// Comparison: arith_expr ~ (comp_op ~ arith_expr)* +// This demonstrates using a combined operator rule for cleaner AST +comp_op = { eq | neq | lt | gt } +comparison = { arith_expr ~ (comp_op ~ arith_expr)* } + +// Top-level expression +expr = { comparison } diff --git a/derive/examples/expression_parser.rs b/derive/examples/expression_parser.rs new file mode 100644 index 0000000..6cadb91 --- /dev/null +++ b/derive/examples/expression_parser.rs @@ -0,0 +1,626 @@ +//! Example demonstrating advanced pest_ast patterns: +//! +//! 1. **Repetition of anonymous sequences**: How to handle patterns like +//! `comparison = { arith_expr ~ (comp_op ~ arith_expr)* }` where you have +//! repeated pairs of (operator, operand) that don't have their own grammar rule. +//! +//! 2. **Nested choices**: How to handle patterns like +//! `arith_expr = { term ~ ((plus|minus) ~ term)* }` where operators are +//! defined as a choice between multiple alternatives. +//! +//! 3. **Parsing into enums**: How to use `FromPest` with enums to represent +//! operator variants and expression types. +//! +//! ## Key Patterns Shown +//! +//! ### Pattern 1: Named Operator Rules -> Enum (with derive) +//! When you have `plus = { "+" }` and `minus = { "-" }` as separate rules, +//! you can derive `FromPest` for each and combine them in an enum that +//! also derives `FromPest`: +//! ```ignore +//! #[derive(FromPest)] +//! #[pest_ast(rule(Rule::plus))] +//! struct Plus; +//! +//! #[derive(FromPest)] +//! #[pest_ast(rule(Rule::minus))] +//! struct Minus; +//! +//! // The enum uses the parent rule that contains the choice +//! #[derive(FromPest)] +//! #[pest_ast(rule(Rule::arith_expr))] // or the containing rule +//! enum AddOp { Plus(Plus), Minus(Minus) } +//! ``` +//! +//! ### Pattern 2: Combined Operator Rule -> Enum (manual FromPest) +//! For `comp_op = { eq | neq | lt | gt }`, implement `FromPest` manually +//! to map the child rules to enum variants. This is useful when you want +//! a simpler enum without wrapper structs. +//! +//! ### Pattern 3: Repetition with Operator-Operand Pairs (manual FromPest) +//! For `term ~ ((plus|minus) ~ term)*`, the `(operator, operand)` pairs +//! don't have their own grammar rule, so you must implement `FromPest` +//! manually to consume pairs of tokens: +//! ```ignore +//! impl FromPest for ArithTail { +//! fn from_pest(pest: &mut Pairs) -> Result { +//! let op = AddOp::from_pest(pest)?; // consume operator +//! let term = Term::from_pest(pest)?; // consume operand +//! Ok(ArithTail { op, term }) +//! } +//! } +//! ``` + +#![allow( + bad_style, + dead_code, + clippy::clone_on_copy, + clippy::upper_case_acronyms +)] + +#[macro_use] +extern crate pest_derive; +extern crate from_pest; +#[macro_use] +extern crate pest_ast; +extern crate pest; + +use from_pest::FromPest; +use pest::Parser; + +#[derive(Parser)] +#[grammar = "../examples/expression_parser.pest"] +pub struct ExprParser; + +// ============================================================================= +// AST Types +// ============================================================================= + +/// Helper to convert a Span to a string slice. +fn span_into_str(span: pest::Span<'_>) -> &str { + span.as_str() +} + +// ----------------------------------------------------------------------------- +// Literals +// ----------------------------------------------------------------------------- + +/// A numeric literal like "42" or "123". +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::number))] +pub struct Number { + /// The numeric value parsed from the input. + #[pest_ast(outer(with(span_into_str), with(str::parse), with(Result::unwrap)))] + pub value: i64, +} + +/// An identifier like "x" or "foo_bar". +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::identifier))] +pub struct Identifier<'pest> { + /// The span containing the identifier text. + #[pest_ast(outer())] + pub span: pest::Span<'pest>, +} + +impl<'pest> Identifier<'pest> { + /// Get the identifier name as a string slice. + pub fn name(&self) -> &str { + self.span.as_str() + } +} + +// ----------------------------------------------------------------------------- +// Atomic Expressions +// ----------------------------------------------------------------------------- + +/// An atomic expression: number, identifier, or parenthesized expression. +/// +/// **Key Pattern**: Using an enum to represent grammar alternatives. +/// The enum derives `FromPest` with the parent rule, and each variant +/// wraps a type that can parse the alternative. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::atom))] +pub enum Atom<'pest> { + /// A numeric literal. + Number(Number), + /// A variable/identifier reference. + Identifier(Identifier<'pest>), + /// A parenthesized expression (recursive). + Parenthesized(Box>), +} + +// ----------------------------------------------------------------------------- +// Multiplicative Operators: mul | div +// Pattern: Each operator has its own rule, combined into an enum. +// ----------------------------------------------------------------------------- + +/// The multiplication operator `*`. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::mul))] +pub struct Mul; + +/// The division operator `/`. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::div))] +pub struct Div; + +/// Multiplicative operators: `*` or `/`. +/// +/// **Key Pattern**: Manual `FromPest` for operators that appear as children +/// of a parent rule. We check if the next token is `mul` or `div` and +/// consume it accordingly. +#[derive(Debug, Clone, PartialEq)] +pub enum MulOp { + /// Multiplication. + Mul(Mul), + /// Division. + Div(Div), +} + +impl<'pest> FromPest<'pest> for MulOp { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + // Try to parse as Mul first + if let Ok(mul) = Mul::from_pest(pest) { + return Ok(MulOp::Mul(mul)); + } + // Try Div + if let Ok(div) = Div::from_pest(pest) { + return Ok(MulOp::Div(div)); + } + Err(from_pest::ConversionError::NoMatch) + } +} + +// ----------------------------------------------------------------------------- +// Additive Operators: plus | minus +// Pattern: Each operator has its own rule, combined into an enum. +// ----------------------------------------------------------------------------- + +/// The addition operator `+`. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::plus))] +pub struct Plus; + +/// The subtraction operator `-`. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::minus))] +pub struct Minus; + +/// Additive operators: `+` or `-`. +/// +/// **Key Pattern**: Manual `FromPest` to try each operator variant. +/// This is necessary because there's no grammar rule specifically for +/// "plus or minus" - they appear directly as children of `arith_expr`. +#[derive(Debug, Clone, PartialEq)] +pub enum AddOp { + /// Addition. + Plus(Plus), + /// Subtraction. + Minus(Minus), +} + +impl<'pest> FromPest<'pest> for AddOp { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + // Try Plus first + if let Ok(plus) = Plus::from_pest(pest) { + return Ok(AddOp::Plus(plus)); + } + // Try Minus + if let Ok(minus) = Minus::from_pest(pest) { + return Ok(AddOp::Minus(minus)); + } + Err(from_pest::ConversionError::NoMatch) + } +} + +// ----------------------------------------------------------------------------- +// Comparison Operators: eq | neq | lt | gt +// Pattern: Combined rule with manual FromPest implementation. +// ----------------------------------------------------------------------------- + +/// Comparison operators. +/// +/// **Key Pattern**: When you have a combined operator rule like +/// `comp_op = { eq | neq | lt | gt }`, you can implement `FromPest` +/// manually to inspect the child rule and map to enum variants. +/// This avoids needing wrapper structs for each operator. +#[derive(Debug, Clone, PartialEq)] +pub enum CompOp { + /// Equality `==`. + Eq, + /// Inequality `!=`. + Neq, + /// Less than `<`. + Lt, + /// Greater than `>`. + Gt, +} + +impl<'pest> FromPest<'pest> for CompOp { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + let pair = pest.next().ok_or(from_pest::ConversionError::NoMatch)?; + if pair.as_rule() == Rule::comp_op { + let inner = pair + .into_inner() + .next() + .ok_or(from_pest::ConversionError::NoMatch)?; + match inner.as_rule() { + Rule::eq => Ok(CompOp::Eq), + Rule::neq => Ok(CompOp::Neq), + Rule::lt => Ok(CompOp::Lt), + Rule::gt => Ok(CompOp::Gt), + _ => Err(from_pest::ConversionError::NoMatch), + } + } else { + Err(from_pest::ConversionError::NoMatch) + } + } +} + +// ----------------------------------------------------------------------------- +// Factor +// ----------------------------------------------------------------------------- + +/// A factor is an atomic expression. +/// In a more complete grammar, this might include unary operators. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::factor))] +pub struct Factor<'pest> { + /// The atomic expression. + pub atom: Atom<'pest>, +} + +// ----------------------------------------------------------------------------- +// Term: factor ~ ((mul | div) ~ factor)* +// Pattern: Manual FromPest for the (operator, operand) tail. +// ----------------------------------------------------------------------------- + +/// One `(operator, operand)` pair in a term's repetition. +/// +/// **Key Pattern for Anonymous Sequences**: +/// For grammar `term = { factor ~ ((mul | div) ~ factor)* }`, the repeated +/// `(mul | div) ~ factor` pairs don't have their own rule name. +/// We implement `FromPest` manually to consume an operator followed by a factor. +#[derive(Debug, Clone, PartialEq)] +pub struct TermTail<'pest> { + /// The multiplicative operator. + pub op: MulOp, + /// The right-hand operand. + pub factor: Factor<'pest>, +} + +impl<'pest> FromPest<'pest> for TermTail<'pest> { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + // First try to get an operator - if not present, no match + let op = MulOp::from_pest(pest)?; + // Then get the factor - if operator succeeded, factor must succeed + let factor = Factor::from_pest(pest) + .map_err(|_| from_pest::ConversionError::NoMatch)?; + Ok(TermTail { op, factor }) + } +} + +/// A term: `factor ~ ((mul | div) ~ factor)*`. +/// +/// **Example**: `2 * 3 / 4` parses as: +/// - `first` = Factor(2) +/// - `rest` = [TermTail(Mul, Factor(3)), TermTail(Div, Factor(4))] +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::term))] +pub struct Term<'pest> { + /// The first factor. + pub first: Factor<'pest>, + /// Additional (operator, factor) pairs. + pub rest: Vec>, +} + +// ----------------------------------------------------------------------------- +// Arithmetic Expression: term ~ ((plus | minus) ~ term)* +// Pattern: Same as Term but with additive operators. +// ----------------------------------------------------------------------------- + +/// One `(operator, operand)` pair in an arithmetic expression. +/// +/// **Key Pattern**: Manual `FromPest` to consume operator + operand pairs. +#[derive(Debug, Clone, PartialEq)] +pub struct ArithTail<'pest> { + /// The additive operator. + pub op: AddOp, + /// The right-hand operand. + pub term: Term<'pest>, +} + +impl<'pest> FromPest<'pest> for ArithTail<'pest> { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + let op = AddOp::from_pest(pest)?; + let term = Term::from_pest(pest) + .map_err(|_| from_pest::ConversionError::NoMatch)?; + Ok(ArithTail { op, term }) + } +} + +/// An arithmetic expression: `term ~ ((plus | minus) ~ term)*`. +/// +/// **Example**: `1 + 2 - 3` parses as: +/// - `first` = Term(1) +/// - `rest` = [ArithTail(Plus, Term(2)), ArithTail(Minus, Term(3))] +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::arith_expr))] +pub struct ArithExpr<'pest> { + /// The first term. + pub first: Term<'pest>, + /// Additional (operator, term) pairs. + pub rest: Vec>, +} + +// ----------------------------------------------------------------------------- +// Comparison Expression: arith_expr ~ (comp_op ~ arith_expr)* +// Pattern: Using a combined operator rule. +// ----------------------------------------------------------------------------- + +/// One `(operator, operand)` pair in a comparison expression. +#[derive(Debug, Clone, PartialEq)] +pub struct CompTail<'pest> { + /// The comparison operator. + pub op: CompOp, + /// The right-hand operand. + pub expr: ArithExpr<'pest>, +} + +impl<'pest> FromPest<'pest> for CompTail<'pest> { + type Rule = Rule; + type FatalError = from_pest::Void; + + fn from_pest( + pest: &mut pest::iterators::Pairs<'pest, Rule>, + ) -> Result> { + let op = CompOp::from_pest(pest)?; + let expr = ArithExpr::from_pest(pest) + .map_err(|_| from_pest::ConversionError::NoMatch)?; + Ok(CompTail { op, expr }) + } +} + +/// A comparison expression: `arith_expr ~ (comp_op ~ arith_expr)*`. +/// +/// **Example**: `1 < 2 == 3` parses as: +/// - `first` = ArithExpr(1) +/// - `rest` = [CompTail(Lt, ArithExpr(2)), CompTail(Eq, ArithExpr(3))] +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::comparison))] +pub struct Comparison<'pest> { + /// The first arithmetic expression. + pub first: ArithExpr<'pest>, + /// Additional (operator, expression) pairs. + pub rest: Vec>, +} + +// ----------------------------------------------------------------------------- +// Top-level Expression +// ----------------------------------------------------------------------------- + +/// The top-level expression wrapper. +#[derive(FromPest, Debug, Clone, PartialEq)] +#[pest_ast(rule(Rule::expr))] +pub struct Expr<'pest> { + /// The comparison expression. + pub comparison: Comparison<'pest>, +} + +// ============================================================================= +// Example Usage +// ============================================================================= + +fn main() -> Result<(), Box> { + println!("=== Expression Parser Example ===\n"); + println!("This example demonstrates:\n"); + println!("1. Repetition of anonymous sequences (operator ~ operand)*"); + println!("2. Nested choices for operators (plus | minus)"); + println!("3. Parsing into enums\n"); + println!("-------------------------------------------\n"); + + // Example 1: Simple arithmetic + let input1 = "1 + 2 * 3"; + println!("Input: {input1:?}"); + let pairs = ExprParser::parse(Rule::expr, input1)?; + let expr: Expr = Expr::from_pest(&mut pairs.clone())?; + println!("Parsed AST: {expr:#?}"); + println!(); + + // Example 2: Comparison with arithmetic + let input2 = "x + 1 < y * 2"; + println!("Input: {input2:?}"); + let pairs = ExprParser::parse(Rule::expr, input2)?; + let expr: Expr = Expr::from_pest(&mut pairs.clone())?; + println!("Parsed AST: {expr:#?}"); + println!(); + + // Example 3: Chained comparisons + let input3 = "a == b != c"; + println!("Input: {input3:?}"); + let pairs = ExprParser::parse(Rule::expr, input3)?; + let expr: Expr = Expr::from_pest(&mut pairs.clone())?; + println!("Parsed AST: {expr:#?}"); + + Ok(()) +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_number_parsing() { + let input = "42"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + // Should parse as: Expr -> Comparison -> ArithExpr -> Term -> Factor -> Atom::Number(42) + if let Atom::Number(n) = &expr.comparison.first.first.first.atom { + assert_eq!(n.value, 42); + } else { + panic!("Expected Number, got {:?}", expr.comparison.first.first.first.atom); + } + } + + #[test] + fn test_identifier_parsing() { + let input = "foo"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + if let Atom::Identifier(id) = &expr.comparison.first.first.first.atom { + assert_eq!(id.name(), "foo"); + } else { + panic!("Expected Identifier"); + } + } + + #[test] + fn test_addition() { + let input = "1 + 2"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + let arith = &expr.comparison.first; + assert_eq!(arith.rest.len(), 1); + assert!(matches!(arith.rest[0].op, AddOp::Plus(_))); + } + + #[test] + fn test_subtraction() { + let input = "5 - 3"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + let arith = &expr.comparison.first; + assert_eq!(arith.rest.len(), 1); + assert!(matches!(arith.rest[0].op, AddOp::Minus(_))); + } + + #[test] + fn test_multiplication() { + let input = "2 * 3"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + let term = &expr.comparison.first.first; + assert_eq!(term.rest.len(), 1); + assert!(matches!(term.rest[0].op, MulOp::Mul(_))); + } + + #[test] + fn test_division() { + let input = "10 / 2"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + let term = &expr.comparison.first.first; + assert_eq!(term.rest.len(), 1); + assert!(matches!(term.rest[0].op, MulOp::Div(_))); + } + + #[test] + fn test_comparison_operators() { + for (input, expected_op) in [ + ("1 == 2", CompOp::Eq), + ("1 != 2", CompOp::Neq), + ("1 < 2", CompOp::Lt), + ("1 > 2", CompOp::Gt), + ] { + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + assert_eq!(expr.comparison.rest.len(), 1, "Input: {input}"); + assert_eq!(expr.comparison.rest[0].op, expected_op, "Input: {input}"); + } + } + + #[test] + fn test_chained_arithmetic() { + // 1 + 2 - 3 + 4 + let input = "1 + 2 - 3 + 4"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + let arith = &expr.comparison.first; + assert_eq!(arith.rest.len(), 3); + assert!(matches!(arith.rest[0].op, AddOp::Plus(_))); + assert!(matches!(arith.rest[1].op, AddOp::Minus(_))); + assert!(matches!(arith.rest[2].op, AddOp::Plus(_))); + } + + #[test] + fn test_chained_comparisons() { + // a < b == c != d + let input = "a < b == c != d"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + assert_eq!(expr.comparison.rest.len(), 3); + assert_eq!(expr.comparison.rest[0].op, CompOp::Lt); + assert_eq!(expr.comparison.rest[1].op, CompOp::Eq); + assert_eq!(expr.comparison.rest[2].op, CompOp::Neq); + } + + #[test] + fn test_operator_precedence_structure() { + // 1 + 2 * 3 should parse with * binding tighter than + + // Structure: ArithExpr { first: Term(1), rest: [(+, Term(2 * 3))] } + let input = "1 + 2 * 3"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + // The first term should be just "1" + assert_eq!(expr.comparison.first.first.rest.len(), 0); + + // The second term (after +) should be "2 * 3" + assert_eq!(expr.comparison.first.rest.len(), 1); + let second_term = &expr.comparison.first.rest[0].term; + assert_eq!(second_term.rest.len(), 1); + assert!(matches!(second_term.rest[0].op, MulOp::Mul(_))); + } + + #[test] + fn test_parenthesized_expression() { + let input = "(1 + 2)"; + let pairs = ExprParser::parse(Rule::expr, input).unwrap(); + let expr: Expr = Expr::from_pest(&mut pairs.clone()).unwrap(); + + // The atom should be Parenthesized containing an Expr + assert!(matches!( + expr.comparison.first.first.first.atom, + Atom::Parenthesized(_) + )); + } +} From de036aba9febf36917185165baac1160a46acb88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:43:54 +0000 Subject: [PATCH 3/3] Fix cargo fmt and clippy issues Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- derive/examples/expression_parser.rs | 14 +++++++------- examples/csv.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/derive/examples/expression_parser.rs b/derive/examples/expression_parser.rs index 6cadb91..0342d38 100644 --- a/derive/examples/expression_parser.rs +++ b/derive/examples/expression_parser.rs @@ -315,8 +315,7 @@ impl<'pest> FromPest<'pest> for TermTail<'pest> { // First try to get an operator - if not present, no match let op = MulOp::from_pest(pest)?; // Then get the factor - if operator succeeded, factor must succeed - let factor = Factor::from_pest(pest) - .map_err(|_| from_pest::ConversionError::NoMatch)?; + let factor = Factor::from_pest(pest).map_err(|_| from_pest::ConversionError::NoMatch)?; Ok(TermTail { op, factor }) } } @@ -359,8 +358,7 @@ impl<'pest> FromPest<'pest> for ArithTail<'pest> { pest: &mut pest::iterators::Pairs<'pest, Rule>, ) -> Result> { let op = AddOp::from_pest(pest)?; - let term = Term::from_pest(pest) - .map_err(|_| from_pest::ConversionError::NoMatch)?; + let term = Term::from_pest(pest).map_err(|_| from_pest::ConversionError::NoMatch)?; Ok(ArithTail { op, term }) } } @@ -401,8 +399,7 @@ impl<'pest> FromPest<'pest> for CompTail<'pest> { pest: &mut pest::iterators::Pairs<'pest, Rule>, ) -> Result> { let op = CompOp::from_pest(pest)?; - let expr = ArithExpr::from_pest(pest) - .map_err(|_| from_pest::ConversionError::NoMatch)?; + let expr = ArithExpr::from_pest(pest).map_err(|_| from_pest::ConversionError::NoMatch)?; Ok(CompTail { op, expr }) } } @@ -489,7 +486,10 @@ mod tests { if let Atom::Number(n) = &expr.comparison.first.first.first.atom { assert_eq!(n.value, 42); } else { - panic!("Expected Number, got {:?}", expr.comparison.first.first.first.atom); + panic!( + "Expected Number, got {:?}", + expr.comparison.first.first.first.atom + ); } } diff --git a/examples/csv.rs b/examples/csv.rs index 135c6ef..37e1d76 100644 --- a/examples/csv.rs +++ b/examples/csv.rs @@ -19,7 +19,7 @@ mod ast { use super::csv::Rule; use pest::Span; - fn span_into_str(span: Span) -> &str { + fn span_into_str(span: Span<'_>) -> &str { span.as_str() }