Skip to main content

shape_ast/parser/expressions/
binary_ops.rs

1//! Binary operator expression parsing
2//!
3//! This module handles parsing of all binary operators with proper precedence:
4//! - Pipe operator (|>)
5//! - Ternary operator (?:)
6//! - Null coalescing (??)
7//! - Error context wrapping (!!)
8//! - Logical operators (||, &&)
9//! - Comparison operators (>, <, >=, <=, ==, !=, ~=, ~>, ~<)
10//! - Range operator (..)
11//! - Arithmetic operators (+, -, *, /, %, ^)
12
13use super::super::pair_span;
14use crate::ast::operators::{FuzzyOp, FuzzyTolerance};
15use crate::ast::{AssignExpr, BinaryOp, Expr, IfExpr, Literal, RangeKind, Span, UnaryOp};
16use crate::error::{Result, ShapeError};
17use crate::parser::{Rule, pair_location};
18use pest::iterators::Pair;
19
20// ---------------------------------------------------------------------------
21// Generic helpers
22// ---------------------------------------------------------------------------
23
24/// Parse a left-associative binary chain: `first (op second)*`.
25///
26/// The Pest rule emits a flat list of children that are all the same sub-rule
27/// (the operators are implicit).  `parse_child` is called for every child and
28/// `op` is the `BinaryOp` that joins them.
29fn parse_binary_chain(
30    pair: Pair<Rule>,
31    error_ctx: &str,
32    op: BinaryOp,
33    parse_child: fn(Pair<Rule>) -> Result<Expr>,
34) -> Result<Expr> {
35    let span = pair_span(&pair);
36    let pair_loc = pair_location(&pair);
37    let mut inner = pair.into_inner();
38    let first = inner.next().ok_or_else(|| ShapeError::ParseError {
39        message: format!("expected expression in {}", error_ctx),
40        location: Some(pair_loc),
41    })?;
42    let mut left = parse_child(first)?;
43
44    for child in inner {
45        let right = parse_child(child)?;
46        left = Expr::BinaryOp {
47            left: Box::new(left),
48            op,
49            right: Box::new(right),
50            span,
51        };
52    }
53
54    Ok(left)
55}
56
57/// Parse an expression that uses string-position-based operator extraction.
58///
59/// This covers `additive_expr`, `shift_expr`, and `multiplicative_expr` where
60/// the Pest grammar emits only the operand sub-rules (no explicit operator
61/// pairs) and the operators must be recovered from the raw source text between
62/// operand spans.
63fn parse_positional_op_chain(
64    pair: Pair<Rule>,
65    error_ctx: &str,
66    parse_child: fn(Pair<Rule>) -> Result<Expr>,
67    resolve_op: fn(&str) -> Result<BinaryOp>,
68) -> Result<Expr> {
69    let span = pair_span(&pair);
70    let expr_str = pair.as_str();
71    let inner_pairs: Vec<_> = pair.into_inner().collect();
72
73    if inner_pairs.is_empty() {
74        return Err(ShapeError::ParseError {
75            message: format!("Empty {} expression", error_ctx),
76            location: None,
77        });
78    }
79
80    let mut left = parse_child(inner_pairs[0].clone())?;
81
82    if inner_pairs.len() == 1 {
83        return Ok(left);
84    }
85
86    let mut current_pos = inner_pairs[0].as_str().len();
87
88    for i in 1..inner_pairs.len() {
89        let expr_start = expr_str[current_pos..]
90            .find(inner_pairs[i].as_str())
91            .ok_or_else(|| ShapeError::ParseError {
92                message: "Cannot find expression in string".to_string(),
93                location: None,
94            })?;
95        let op_str = expr_str[current_pos..current_pos + expr_start].trim();
96        let op = resolve_op(op_str)?;
97        let right = parse_child(inner_pairs[i].clone())?;
98
99        left = Expr::BinaryOp {
100            left: Box::new(left),
101            op,
102            right: Box::new(right),
103            span,
104        };
105
106        current_pos += expr_start + inner_pairs[i].as_str().len();
107    }
108
109    Ok(left)
110}
111
112// ---------------------------------------------------------------------------
113// Precedence-level dispatch helpers (range / no-range)
114// ---------------------------------------------------------------------------
115
116/// The precedence chain is:
117///
118///   null_coalesce -> context -> or -> and -> bitwise_or -> bitwise_xor
119///     -> bitwise_and -> comparison -> [range ->] additive -> shift
120///     -> multiplicative -> exponential -> unary
121///
122/// The only difference between the range and no-range chains is that
123/// comparison delegates to `parse_range_expr` (which then delegates to
124/// additive) when ranges are allowed, and directly to `parse_additive_expr`
125/// when they are not.
126
127fn select_null_coalesce(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
128    if allow_range { parse_null_coalesce_expr } else { parse_null_coalesce_expr_no_range }
129}
130fn child_of_null_coalesce(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
131    if allow_range { parse_context_expr } else { parse_context_expr_no_range }
132}
133fn child_of_context(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
134    if allow_range { parse_or_expr } else { parse_or_expr_no_range }
135}
136fn child_of_or(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
137    if allow_range { parse_and_expr } else { parse_and_expr_no_range }
138}
139fn child_of_and(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
140    if allow_range { parse_bitwise_or_expr } else { parse_bitwise_or_expr_no_range }
141}
142fn child_of_bitwise_or(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
143    if allow_range { parse_bitwise_xor_expr } else { parse_bitwise_xor_expr_no_range }
144}
145fn child_of_bitwise_xor(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
146    if allow_range { parse_bitwise_and_expr } else { parse_bitwise_and_expr_no_range }
147}
148fn child_of_bitwise_and(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
149    if allow_range { parse_comparison_expr } else { parse_comparison_expr_no_range }
150}
151fn child_of_comparison(allow_range: bool) -> fn(Pair<Rule>) -> Result<Expr> {
152    if allow_range { parse_range_expr } else { parse_additive_expr }
153}
154
155// ---------------------------------------------------------------------------
156// Pipe (not duplicated -- ranges are allowed in pipe context)
157// ---------------------------------------------------------------------------
158
159/// Parse pipe expression (a |> b |> c)
160/// Pipes the left value into the right function
161pub fn parse_pipe_expr(pair: Pair<Rule>) -> Result<Expr> {
162    parse_binary_chain(pair, "pipe", BinaryOp::Pipe, parse_ternary_expr)
163}
164
165// ---------------------------------------------------------------------------
166// Ternary (condition ? then : else)
167// ---------------------------------------------------------------------------
168
169/// Parse ternary expression (condition ? then : else)
170pub fn parse_ternary_expr(pair: Pair<Rule>) -> Result<Expr> {
171    parse_ternary_impl(pair, true)
172}
173
174fn parse_ternary_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
175    parse_ternary_impl(pair, false)
176}
177
178fn parse_ternary_impl(pair: Pair<Rule>, allow_range: bool) -> Result<Expr> {
179    let span = pair_span(&pair);
180    let pair_loc = pair_location(&pair);
181    let mut inner = pair.into_inner();
182    let condition_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
183        message: "expected condition expression in ternary".to_string(),
184        location: Some(pair_loc.clone()),
185    })?;
186    let condition_expr = (select_null_coalesce(allow_range))(condition_pair)?;
187
188    if let Some(then_pair) = inner.next() {
189        let then_expr = parse_ternary_branch(then_pair)?;
190        let else_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
191            message: "expected else expression after ':' in ternary".to_string(),
192            location: Some(pair_loc),
193        })?;
194        let else_expr = parse_ternary_branch(else_pair)?;
195
196        Ok(Expr::If(
197            Box::new(IfExpr {
198                condition: Box::new(condition_expr),
199                then_branch: Box::new(then_expr),
200                else_branch: Some(Box::new(else_expr)),
201            }),
202            span,
203        ))
204    } else {
205        Ok(condition_expr)
206    }
207}
208
209fn parse_ternary_branch(pair: Pair<Rule>) -> Result<Expr> {
210    let pair_loc = pair_location(&pair);
211    match pair.as_rule() {
212        Rule::ternary_branch => {
213            let inner = pair
214                .into_inner()
215                .next()
216                .ok_or_else(|| ShapeError::ParseError {
217                    message: "expected expression in ternary branch".to_string(),
218                    location: Some(pair_loc),
219                })?;
220            parse_ternary_expr_no_range(inner)
221        }
222        Rule::ternary_expr_no_range => parse_ternary_expr_no_range(pair),
223        Rule::assignment_expr_no_range => parse_assignment_expr_no_range(pair),
224        _ => super::primary::parse_expression(pair),
225    }
226}
227
228// ---------------------------------------------------------------------------
229// Assignment (target = value, target += value)
230// ---------------------------------------------------------------------------
231
232fn compound_op_to_binary(op_str: &str) -> Option<BinaryOp> {
233    match op_str {
234        "+=" => Some(BinaryOp::Add),
235        "-=" => Some(BinaryOp::Sub),
236        "*=" => Some(BinaryOp::Mul),
237        "/=" => Some(BinaryOp::Div),
238        "%=" => Some(BinaryOp::Mod),
239        "**=" => Some(BinaryOp::Pow),
240        "^=" => Some(BinaryOp::BitXor),
241        "&=" => Some(BinaryOp::BitAnd),
242        "|=" => Some(BinaryOp::BitOr),
243        "<<=" => Some(BinaryOp::BitShl),
244        ">>=" => Some(BinaryOp::BitShr),
245        _ => None,
246    }
247}
248
249/// Parse assignment expression (target = value or target += value)
250pub fn parse_assignment_expr(pair: Pair<Rule>) -> Result<Expr> {
251    parse_assignment_impl(pair, true)
252}
253
254fn parse_assignment_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
255    parse_assignment_impl(pair, false)
256}
257
258fn parse_assignment_impl(pair: Pair<Rule>, allow_range: bool) -> Result<Expr> {
259    let span = pair_span(&pair);
260    let pair_loc = pair_location(&pair);
261    let mut inner = pair.into_inner();
262    let first = inner.next().ok_or_else(|| ShapeError::ParseError {
263        message: "expected expression".to_string(),
264        location: Some(pair_loc.clone()),
265    })?;
266
267    let recurse: fn(Pair<Rule>) -> Result<Expr> = if allow_range {
268        parse_assignment_expr
269    } else {
270        parse_assignment_expr_no_range
271    };
272
273    if let Some(second) = inner.next() {
274        if second.as_rule() == Rule::compound_assign_op {
275            let target = super::primary::parse_postfix_expr(first)?;
276            if !matches!(
277                target,
278                Expr::Identifier(_, _) | Expr::PropertyAccess { .. } | Expr::IndexAccess { .. }
279            ) {
280                return Err(ShapeError::ParseError {
281                    message: "invalid assignment target".to_string(),
282                    location: Some(pair_loc),
283                });
284            }
285            let bin_op =
286                compound_op_to_binary(second.as_str()).ok_or_else(|| ShapeError::ParseError {
287                    message: format!("Unknown compound operator: {}", second.as_str()),
288                    location: None,
289                })?;
290            let value_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
291                message: "expected value after compound assignment".to_string(),
292                location: None,
293            })?;
294            let value = recurse(value_pair)?;
295            let desugared = Expr::BinaryOp {
296                left: Box::new(target.clone()),
297                op: bin_op,
298                right: Box::new(value),
299                span,
300            };
301            Ok(Expr::Assign(
302                Box::new(AssignExpr {
303                    target: Box::new(target),
304                    value: Box::new(desugared),
305                }),
306                span,
307            ))
308        } else if second.as_rule() == Rule::assign_op {
309            let target = super::primary::parse_postfix_expr(first)?;
310            if !matches!(
311                target,
312                Expr::Identifier(_, _) | Expr::PropertyAccess { .. } | Expr::IndexAccess { .. }
313            ) {
314                return Err(ShapeError::ParseError {
315                    message: "invalid assignment target".to_string(),
316                    location: Some(pair_loc),
317                });
318            }
319            let value_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
320                message: "expected value after assignment".to_string(),
321                location: None,
322            })?;
323            let value = recurse(value_pair)?;
324            Ok(Expr::Assign(
325                Box::new(AssignExpr {
326                    target: Box::new(target),
327                    value: Box::new(value),
328                }),
329                span,
330            ))
331        } else if allow_range {
332            match first.as_rule() {
333                Rule::pipe_expr => parse_pipe_expr(first),
334                Rule::ternary_expr => parse_ternary_expr(first),
335                _ => parse_pipe_expr(first),
336            }
337        } else {
338            (select_null_coalesce(false))(first)
339        }
340    } else if allow_range {
341        match first.as_rule() {
342            Rule::pipe_expr => parse_pipe_expr(first),
343            Rule::ternary_expr => parse_ternary_expr(first),
344            _ => parse_pipe_expr(first),
345        }
346    } else {
347        (select_null_coalesce(false))(first)
348    }
349}
350
351// ---------------------------------------------------------------------------
352// Null coalescing (a ?? b)
353// ---------------------------------------------------------------------------
354
355/// Parse null coalescing expression (a ?? b)
356pub fn parse_null_coalesce_expr(pair: Pair<Rule>) -> Result<Expr> {
357    parse_binary_chain(pair, "null coalesce", BinaryOp::NullCoalesce, child_of_null_coalesce(true))
358}
359
360fn parse_null_coalesce_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
361    parse_binary_chain(pair, "null coalesce", BinaryOp::NullCoalesce, child_of_null_coalesce(false))
362}
363
364// ---------------------------------------------------------------------------
365// Error context (a !! b) -- special TryOperator handling
366// ---------------------------------------------------------------------------
367
368/// Parse error context expression (lhs !! rhs).
369pub fn parse_context_expr(pair: Pair<Rule>) -> Result<Expr> {
370    parse_context_impl(pair, true)
371}
372
373fn parse_context_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
374    parse_context_impl(pair, false)
375}
376
377fn parse_context_impl(pair: Pair<Rule>, allow_range: bool) -> Result<Expr> {
378    let span = pair_span(&pair);
379    let pair_loc = pair_location(&pair);
380    let parse_child = child_of_context(allow_range);
381    let mut inner = pair.into_inner();
382    let first = inner.next().ok_or_else(|| ShapeError::ParseError {
383        message: "expected expression in error context".to_string(),
384        location: Some(pair_loc),
385    })?;
386    let mut left = parse_child(first)?;
387
388    for or_expr in inner {
389        let rhs_source = or_expr.as_str().trim().to_string();
390        let right = parse_child(or_expr)?;
391        let is_grouped_rhs = rhs_source.starts_with('(') && rhs_source.ends_with(')');
392
393        match right {
394            Expr::TryOperator(inner_try, try_span) if !is_grouped_rhs => {
395                let context_expr = Expr::BinaryOp {
396                    left: Box::new(left),
397                    op: BinaryOp::ErrorContext,
398                    right: inner_try,
399                    span,
400                };
401                left = Expr::TryOperator(Box::new(context_expr), try_span);
402            }
403            right => {
404                left = Expr::BinaryOp {
405                    left: Box::new(left),
406                    op: BinaryOp::ErrorContext,
407                    right: Box::new(right),
408                    span,
409                };
410            }
411        }
412    }
413
414    Ok(left)
415}
416
417// ---------------------------------------------------------------------------
418// Logical OR / AND, Bitwise OR / XOR / AND
419// ---------------------------------------------------------------------------
420
421/// Parse logical OR expression (a || b)
422pub fn parse_or_expr(pair: Pair<Rule>) -> Result<Expr> {
423    parse_binary_chain(pair, "logical OR", BinaryOp::Or, child_of_or(true))
424}
425fn parse_or_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
426    parse_binary_chain(pair, "logical OR", BinaryOp::Or, child_of_or(false))
427}
428
429/// Parse logical AND expression (a && b)
430pub fn parse_and_expr(pair: Pair<Rule>) -> Result<Expr> {
431    parse_binary_chain(pair, "logical AND", BinaryOp::And, child_of_and(true))
432}
433fn parse_and_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
434    parse_binary_chain(pair, "logical AND", BinaryOp::And, child_of_and(false))
435}
436
437/// Parse bitwise OR expression (a | b)
438fn parse_bitwise_or_expr(pair: Pair<Rule>) -> Result<Expr> {
439    parse_binary_chain(pair, "bitwise OR", BinaryOp::BitOr, child_of_bitwise_or(true))
440}
441fn parse_bitwise_or_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
442    parse_binary_chain(pair, "bitwise OR", BinaryOp::BitOr, child_of_bitwise_or(false))
443}
444
445/// Parse bitwise XOR expression (a ^ b)
446fn parse_bitwise_xor_expr(pair: Pair<Rule>) -> Result<Expr> {
447    parse_binary_chain(pair, "bitwise XOR", BinaryOp::BitXor, child_of_bitwise_xor(true))
448}
449fn parse_bitwise_xor_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
450    parse_binary_chain(pair, "bitwise XOR", BinaryOp::BitXor, child_of_bitwise_xor(false))
451}
452
453/// Parse bitwise AND expression (a & b)
454fn parse_bitwise_and_expr(pair: Pair<Rule>) -> Result<Expr> {
455    parse_binary_chain(pair, "bitwise AND", BinaryOp::BitAnd, child_of_bitwise_and(true))
456}
457fn parse_bitwise_and_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
458    parse_binary_chain(pair, "bitwise AND", BinaryOp::BitAnd, child_of_bitwise_and(false))
459}
460
461// ---------------------------------------------------------------------------
462// Comparison (>, <, >=, <=, ==, !=, ~=, ~>, ~<, is)
463// ---------------------------------------------------------------------------
464
465/// Parse comparison expression (a > b, a == b, etc.)
466pub fn parse_comparison_expr(pair: Pair<Rule>) -> Result<Expr> {
467    parse_comparison_impl(pair, true)
468}
469
470fn parse_comparison_expr_no_range(pair: Pair<Rule>) -> Result<Expr> {
471    parse_comparison_impl(pair, false)
472}
473
474fn parse_comparison_impl(pair: Pair<Rule>, allow_range: bool) -> Result<Expr> {
475    let span = pair_span(&pair);
476    let pair_loc = pair_location(&pair);
477    let parse_child = child_of_comparison(allow_range);
478    let mut inner = pair.into_inner();
479    let first = inner.next().ok_or_else(|| ShapeError::ParseError {
480        message: "expected expression in comparison".to_string(),
481        location: Some(pair_loc),
482    })?;
483    let mut left = parse_child(first)?;
484
485    for tail in inner {
486        left = apply_comparison_tail(left, tail, span, parse_child)?;
487    }
488
489    Ok(left)
490}
491
492fn apply_comparison_tail(
493    left: Expr,
494    tail: Pair<Rule>,
495    span: Span,
496    parse_rhs: fn(Pair<Rule>) -> Result<Expr>,
497) -> Result<Expr> {
498    let mut tail_inner = tail.into_inner();
499    let first = tail_inner.next().ok_or_else(|| ShapeError::ParseError {
500        message: "Empty comparison tail".to_string(),
501        location: None,
502    })?;
503
504    match first.as_rule() {
505        Rule::fuzzy_comparison_tail | Rule::fuzzy_comparison_tail_no_range => {
506            let mut fuzzy_inner = first.into_inner();
507
508            let fuzzy_op_pair = fuzzy_inner.next().ok_or_else(|| ShapeError::ParseError {
509                message: "Fuzzy comparison missing operator".to_string(),
510                location: None,
511            })?;
512            let op = parse_fuzzy_op(fuzzy_op_pair)?;
513
514            let rhs_pair = fuzzy_inner.next().ok_or_else(|| ShapeError::ParseError {
515                message: "Fuzzy comparison missing right-hand side".to_string(),
516                location: None,
517            })?;
518            let right = parse_rhs(rhs_pair)?;
519
520            let tolerance = if let Some(within_clause) = fuzzy_inner.next() {
521                parse_within_clause(within_clause)?
522            } else {
523                FuzzyTolerance::Percentage(0.02)
524            };
525
526            Ok(Expr::FuzzyComparison {
527                left: Box::new(left),
528                op,
529                right: Box::new(right),
530                tolerance,
531                span,
532            })
533        }
534        Rule::comparison_op => {
535            let op = parse_comparison_op(first)?;
536            let rhs_pair = tail_inner.next().ok_or_else(|| ShapeError::ParseError {
537                message: "Comparison operator missing right-hand side".to_string(),
538                location: None,
539            })?;
540            let right = parse_rhs(rhs_pair)?;
541            Ok(Expr::BinaryOp {
542                left: Box::new(left),
543                op,
544                right: Box::new(right),
545                span,
546            })
547        }
548        Rule::type_annotation => {
549            let type_annotation = crate::parser::parse_type_annotation(first)?;
550            Ok(Expr::InstanceOf {
551                expr: Box::new(left),
552                type_annotation,
553                span,
554            })
555        }
556        _ => Err(ShapeError::ParseError {
557            message: format!("Unexpected comparison tail: {:?}", first.as_rule()),
558            location: None,
559        }),
560    }
561}
562
563/// Parse fuzzy operator (~=, ~<, ~>)
564fn parse_fuzzy_op(pair: Pair<Rule>) -> Result<FuzzyOp> {
565    match pair.as_str() {
566        "~=" => Ok(FuzzyOp::Equal),
567        "~>" => Ok(FuzzyOp::Greater),
568        "~<" => Ok(FuzzyOp::Less),
569        _ => Err(ShapeError::ParseError {
570            message: format!("Unknown fuzzy operator: {}", pair.as_str()),
571            location: None,
572        }),
573    }
574}
575
576/// Parse within_clause: "within" ~ tolerance_spec
577fn parse_within_clause(pair: Pair<Rule>) -> Result<FuzzyTolerance> {
578    let mut inner = pair.into_inner();
579    let tolerance_spec = inner.next().ok_or_else(|| ShapeError::ParseError {
580        message: "Within clause missing tolerance value".to_string(),
581        location: None,
582    })?;
583    parse_tolerance_spec(tolerance_spec)
584}
585
586/// Parse tolerance_spec: number ~ "%"?
587fn parse_tolerance_spec(pair: Pair<Rule>) -> Result<FuzzyTolerance> {
588    let text = pair.as_str().trim();
589
590    if text.ends_with('%') {
591        let num_str = text.trim_end_matches('%');
592        let value: f64 = num_str.parse().map_err(|_| ShapeError::ParseError {
593            message: format!("Invalid tolerance percentage: {}", text),
594            location: None,
595        })?;
596        Ok(FuzzyTolerance::Percentage(value / 100.0))
597    } else {
598        let value: f64 = text.parse().map_err(|_| ShapeError::ParseError {
599            message: format!("Invalid tolerance value: {}", text),
600            location: None,
601        })?;
602        Ok(FuzzyTolerance::Absolute(value))
603    }
604}
605
606/// Parse comparison operator
607pub fn parse_comparison_op(pair: Pair<Rule>) -> Result<BinaryOp> {
608    match pair.as_str() {
609        ">" => Ok(BinaryOp::Greater),
610        "<" => Ok(BinaryOp::Less),
611        ">=" => Ok(BinaryOp::GreaterEq),
612        "<=" => Ok(BinaryOp::LessEq),
613        "==" => Ok(BinaryOp::Equal),
614        "!=" => Ok(BinaryOp::NotEqual),
615        "~=" => Ok(BinaryOp::FuzzyEqual),
616        "~>" => Ok(BinaryOp::FuzzyGreater),
617        "~<" => Ok(BinaryOp::FuzzyLess),
618        _ => Err(ShapeError::ParseError {
619            message: format!("Unknown comparison operator: {}", pair.as_str()),
620            location: None,
621        }),
622    }
623}
624
625// ---------------------------------------------------------------------------
626// Range (a..b, a..=b, ..b, ..=b, a.., ..)
627// ---------------------------------------------------------------------------
628
629/// Parse a range expression with Rust-style syntax.
630/// Supports: start..end, start..=end, ..end, ..=end, start.., ..
631pub fn parse_range_expr(pair: Pair<Rule>) -> Result<Expr> {
632    let span = pair_span(&pair);
633    let pair_loc = pair_location(&pair);
634    let mut inner = pair.into_inner().peekable();
635
636    let first = inner.next().ok_or_else(|| ShapeError::ParseError {
637        message: "expected expression in range".to_string(),
638        location: Some(pair_loc.clone()),
639    })?;
640
641    match first.as_rule() {
642        Rule::range_op => {
643            let kind = parse_range_op(&first);
644            if let Some(end_pair) = inner.next() {
645                let end = parse_additive_expr(end_pair)?;
646                Ok(Expr::Range { start: None, end: Some(Box::new(end)), kind, span })
647            } else {
648                Ok(Expr::Range { start: None, end: None, kind, span })
649            }
650        }
651        Rule::additive_expr => {
652            let start = parse_additive_expr(first)?;
653            if let Some(next) = inner.next() {
654                match next.as_rule() {
655                    Rule::range_op => {
656                        let kind = parse_range_op(&next);
657                        if let Some(end_pair) = inner.next() {
658                            let end = parse_additive_expr(end_pair)?;
659                            Ok(Expr::Range {
660                                start: Some(Box::new(start)),
661                                end: Some(Box::new(end)),
662                                kind,
663                                span,
664                            })
665                        } else {
666                            Ok(Expr::Range {
667                                start: Some(Box::new(start)),
668                                end: None,
669                                kind,
670                                span,
671                            })
672                        }
673                    }
674                    _ => Err(ShapeError::ParseError {
675                        message: format!(
676                            "unexpected token in range expression: {:?}",
677                            next.as_rule()
678                        ),
679                        location: Some(pair_loc),
680                    }),
681                }
682            } else {
683                Ok(start)
684            }
685        }
686        _ => parse_additive_expr(first),
687    }
688}
689
690fn parse_range_op(pair: &Pair<Rule>) -> RangeKind {
691    if pair.as_str() == "..=" { RangeKind::Inclusive } else { RangeKind::Exclusive }
692}
693
694// ---------------------------------------------------------------------------
695// Additive / Shift / Multiplicative (positional-op-chain pattern)
696// ---------------------------------------------------------------------------
697
698fn resolve_additive_op(op_str: &str) -> Result<BinaryOp> {
699    match op_str {
700        "+" => Ok(BinaryOp::Add),
701        "-" => Ok(BinaryOp::Sub),
702        _ => Err(ShapeError::ParseError {
703            message: format!("Unknown additive operator: '{}'", op_str),
704            location: None,
705        }),
706    }
707}
708
709fn resolve_shift_op(op_str: &str) -> Result<BinaryOp> {
710    match op_str {
711        "<<" => Ok(BinaryOp::BitShl),
712        ">>" => Ok(BinaryOp::BitShr),
713        _ => Err(ShapeError::ParseError {
714            message: format!("Unknown shift operator: '{}'", op_str),
715            location: None,
716        }),
717    }
718}
719
720fn resolve_multiplicative_op(op_str: &str) -> Result<BinaryOp> {
721    match op_str {
722        "*" => Ok(BinaryOp::Mul),
723        "/" => Ok(BinaryOp::Div),
724        "%" => Ok(BinaryOp::Mod),
725        _ => Err(ShapeError::ParseError {
726            message: format!("Unknown multiplicative operator: '{}'", op_str),
727            location: None,
728        }),
729    }
730}
731
732/// Parse additive expression (a + b, a - b)
733pub fn parse_additive_expr(pair: Pair<Rule>) -> Result<Expr> {
734    parse_positional_op_chain(pair, "additive", parse_shift_expr, resolve_additive_op)
735}
736
737/// Parse shift expression (a << b, a >> b)
738pub fn parse_shift_expr(pair: Pair<Rule>) -> Result<Expr> {
739    parse_positional_op_chain(pair, "shift", parse_multiplicative_expr, resolve_shift_op)
740}
741
742/// Parse multiplicative expression (a * b, a / b, a % b)
743pub fn parse_multiplicative_expr(pair: Pair<Rule>) -> Result<Expr> {
744    parse_positional_op_chain(pair, "multiplicative", parse_exponential_expr, resolve_multiplicative_op)
745}
746
747// ---------------------------------------------------------------------------
748// Exponential (right-associative: a ** b ** c = a ** (b ** c))
749// ---------------------------------------------------------------------------
750
751/// Parse exponential expression (a ** b)
752pub fn parse_exponential_expr(pair: Pair<Rule>) -> Result<Expr> {
753    let span = pair_span(&pair);
754    let inner_pairs: Vec<_> = pair.into_inner().collect();
755
756    if inner_pairs.is_empty() {
757        return Err(ShapeError::ParseError {
758            message: "Empty exponential expression".to_string(),
759            location: None,
760        });
761    }
762
763    let mut exprs: Vec<Expr> = Vec::new();
764    for p in inner_pairs {
765        exprs.push(parse_unary_expr(p)?);
766    }
767
768    if exprs.len() == 1 {
769        return Ok(exprs.into_iter().next().unwrap());
770    }
771
772    // Right-associative: a ** b ** c = a ** (b ** c)
773    let mut result = exprs.pop().unwrap();
774    while let Some(left_expr) = exprs.pop() {
775        result = Expr::BinaryOp {
776            left: Box::new(left_expr),
777            op: BinaryOp::Pow,
778            right: Box::new(result),
779            span,
780        };
781    }
782
783    Ok(result)
784}
785
786// ---------------------------------------------------------------------------
787// Unary (!a, -a, ~a, &a, &mut a)
788// ---------------------------------------------------------------------------
789
790/// Parse unary expression (!a, -a)
791pub fn parse_unary_expr(pair: Pair<Rule>) -> Result<Expr> {
792    let span = pair_span(&pair);
793    let pair_str = pair.as_str();
794    let pair_loc = pair_location(&pair);
795    let mut inner = pair.into_inner();
796    let first = inner.next().ok_or_else(|| ShapeError::ParseError {
797        message: "expected expression in unary operation".to_string(),
798        location: Some(pair_loc.clone()),
799    })?;
800
801    // Check if this is a ref_expr rule (& or &mut prefix)
802    if first.as_rule() == Rule::ref_expr {
803        let ref_span = pair_span(&first);
804        let ref_inner = first.into_inner();
805        let mut is_mutable = false;
806        let mut expr_pair = None;
807        for child in ref_inner {
808            match child.as_rule() {
809                Rule::ref_mut_keyword => {
810                    is_mutable = true;
811                }
812                _ => {
813                    expr_pair = Some(child);
814                }
815            }
816        }
817        let inner_expr = expr_pair.ok_or_else(|| ShapeError::ParseError {
818            message: "expected expression after &".to_string(),
819            location: Some(pair_loc),
820        })?;
821        let operand = super::primary::parse_postfix_expr(inner_expr)?;
822        return Ok(Expr::Reference {
823            expr: Box::new(operand),
824            is_mutable,
825            span: ref_span,
826        });
827    }
828
829    if pair_str.starts_with('!') {
830        Ok(Expr::UnaryOp {
831            op: UnaryOp::Not,
832            operand: Box::new(parse_unary_expr(first)?),
833            span,
834        })
835    } else if pair_str.starts_with('~') {
836        Ok(Expr::UnaryOp {
837            op: UnaryOp::BitNot,
838            operand: Box::new(parse_unary_expr(first)?),
839            span,
840        })
841    } else if pair_str.starts_with('-') {
842        let operand = parse_unary_expr(first)?;
843        // Fold negation into typed integer literals so that `-128i8` parses
844        // as a single `TypedInt(-128, I8)` instead of `Neg(TypedInt(128, I8))`.
845        match &operand {
846            Expr::Literal(Literal::TypedInt(value, width), lit_span) => {
847                let neg = value.wrapping_neg();
848                if width.in_range_i64(neg) {
849                    return Ok(Expr::Literal(Literal::TypedInt(neg, *width), *lit_span));
850                }
851            }
852            Expr::Literal(Literal::Int(value), lit_span) => {
853                return Ok(Expr::Literal(Literal::Int(-value), *lit_span));
854            }
855            Expr::Literal(Literal::Number(value), lit_span) => {
856                return Ok(Expr::Literal(Literal::Number(-value), *lit_span));
857            }
858            _ => {}
859        }
860        Ok(Expr::UnaryOp {
861            op: UnaryOp::Neg,
862            operand: Box::new(operand),
863            span,
864        })
865    } else {
866        super::primary::parse_postfix_expr(first)
867    }
868}