Skip to main content

shrub_parser/
arithmetic.rs

1//! Parser for shell arithmetic expressions.
2
3use crate::{ast, error};
4
5/// Parses a shell arithmetic expression.
6///
7/// # Arguments
8///
9/// * `input` - The arithmetic expression to parse, in string form.
10///
11/// # Errors
12///
13/// Returns [`crate::WordParseError::ArithmeticExpression`] if `input` is not a
14/// valid arithmetic expression. That includes a numeric literal that does not
15/// fit in an `i64`, a radix outside 2 to 64, and a digit that is too large for
16/// its radix.
17pub fn parse(input: &str) -> Result<ast::ArithmeticExpr, error::WordParseError> {
18    cacheable_parse(input)
19}
20
21#[cached::macros::cached(max_size = 64, key = "String", convert = r#"{ input.to_owned() }"#)]
22fn cacheable_parse(input: &str) -> Result<ast::ArithmeticExpr, error::WordParseError> {
23    tracing::debug!(target: "arithmetic", "parsing arithmetic expression: '{input}'");
24    arithmetic::full_expression(input)
25        .map_err(|e| error::WordParseError::ArithmeticExpression(e.into()))
26}
27
28peg::parser! {
29    grammar arithmetic() for str {
30        pub(crate) rule full_expression() -> ast::ArithmeticExpr =
31            ![_] { ast::ArithmeticExpr::Literal(0) } /
32            _ e:expression() _ { e }
33
34        pub(crate) rule expression() -> ast::ArithmeticExpr = precedence!{
35            x:(@) _ "," _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Comma, Box::new(x), Box::new(y)) }
36            --
37            x:lvalue() _ "*=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Multiply, x, Box::new(y)) }
38            x:lvalue() _ "/=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Divide, x, Box::new(y)) }
39            x:lvalue() _ "%=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Modulo, x, Box::new(y)) }
40            x:lvalue() _ "+=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Add, x, Box::new(y)) }
41            x:lvalue() _ "-=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Subtract, x, Box::new(y)) }
42            x:lvalue() _ "<<=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftLeft, x, Box::new(y)) }
43            x:lvalue() _ ">>=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftRight, x, Box::new(y)) }
44            x:lvalue() _ "&=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseAnd, x, Box::new(y)) }
45            x:lvalue() _ "|=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseOr, x, Box::new(y)) }
46            x:lvalue() _ "^=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseXor, x, Box::new(y)) }
47            x:lvalue() _ "=" _ y:(@) { ast::ArithmeticExpr::Assignment(x, Box::new(y)) }
48            --
49            x:@ _ "?" _ y:expression() _ ":" _ z:(@) { ast::ArithmeticExpr::Conditional(Box::new(x), Box::new(y), Box::new(z)) }
50            --
51            x:(@) _ "||" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalOr, Box::new(x), Box::new(y)) }
52            --
53            x:(@) _ "&&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalAnd, Box::new(x), Box::new(y)) }
54            --
55            x:(@) _ "|" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseOr, Box::new(x), Box::new(y)) }
56            --
57            x:(@) _ "^" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseXor, Box::new(x), Box::new(y)) }
58            --
59            x:(@) _ "&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseAnd, Box::new(x), Box::new(y)) }
60            --
61            x:(@) _ "==" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Equals, Box::new(x), Box::new(y)) }
62            x:(@) _ "!=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::NotEquals, Box::new(x), Box::new(y)) }
63            --
64            x:(@) _ "<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThan, Box::new(x), Box::new(y)) }
65            x:(@) _ ">" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThan, Box::new(x), Box::new(y)) }
66            x:(@) _ "<=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThanOrEqualTo, Box::new(x), Box::new(y)) }
67            x:(@) _ ">=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThanOrEqualTo, Box::new(x), Box::new(y)) }
68            --
69            x:(@) _ "<<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftLeft, Box::new(x), Box::new(y)) }
70            x:(@) _ ">>" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftRight, Box::new(x), Box::new(y)) }
71            --
72            x:(@) _ "+" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Add, Box::new(x), Box::new(y)) }
73            x:(@) _ "-" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Subtract, Box::new(x), Box::new(y)) }
74            --
75            x:(@) _ "*" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Multiply, Box::new(x), Box::new(y)) }
76            x:(@) _ "%" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Modulo, Box::new(x), Box::new(y)) }
77            x:(@) _ "/" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Divide, Box::new(x), Box::new(y)) }
78            --
79            x:@ _ "**" _ y:(@) { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Power, Box::new(x), Box::new(y)) }
80            --
81            "!" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::LogicalNot, Box::new(x)) }
82            "~" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::BitwiseNot, Box::new(x)) }
83            --
84            // NOTE: The negative lookahead leaves `++x` and `--x` to the pre-increment/pre-decrement
85            // operators. As in bash, `++` or `--` before anything but a variable, such as `++0`, is
86            // two unary operators.
87            "+" !("+" _ lvalue()) _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryPlus, Box::new(x)) }
88            "-" !("-" _ lvalue()) _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryMinus, Box::new(x)) }
89            --
90            "++" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixIncrement, x) }
91            "--" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixDecrement, x) }
92            --
93            x:lvalue() _ "++" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixIncrement, x) }
94            x:lvalue() _ "--" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixDecrement, x) }
95            --
96            n:literal_number() { ast::ArithmeticExpr::Literal(n) }
97            l:lvalue() { ast::ArithmeticExpr::Reference(l) }
98            "(" _ expr:expression() _ ")" { expr }
99        }
100
101        rule lvalue() -> ast::ArithmeticTarget =
102            name:variable_name() "[" index:expression() "]" {
103                ast::ArithmeticTarget::ArrayElement(name.to_owned(), Box::new(index))
104            } /
105            name:variable_name() {
106                ast::ArithmeticTarget::Variable(name.to_owned())
107            }
108
109        rule variable_name() -> &'input str =
110            $(['a'..='z' | 'A'..='Z' | '_'](['a'..='z' | 'A'..='Z' | '_' | '0'..='9']*))
111
112        rule _() -> () = quiet!{[' ' | '\t' | '\n' | '\r']*} {}
113
114        rule literal_number() -> i64 =
115            // Literal with explicit radix (format: <base>#<literal>)
116            radix:decimal_literal() "#" s:$(['0'..='9' | 'a'..='z' | 'A'..='Z' | '@' | '_']+) {?
117                parse_shell_literal_number(s, radix.cast_unsigned())
118            } /
119            // Hex literal
120            "0" ['x' | 'X'] s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*) {?
121                i64::from_str_radix(s, 16).or(Err("i64"))
122            } /
123            // Octal literal
124            s:$("0" ['0'..='8']*) {?
125                i64::from_str_radix(s, 8).or(Err("i64"))
126            } /
127            // Decimal literal
128            decimal_literal()
129
130        rule decimal_literal() -> i64 =
131            s:$(['1'..='9'] ['0'..='9']*) {?
132                // Parse as u64 first, then cast to i64. This handles values like
133                // 9223372036854775808 (i64::MAX + 1) which is needed for INT64_MIN
134                // when preceded by unary minus: -(9223372036854775808) wraps to i64::MIN.
135                s.parse::<u64>().map(|v| v.cast_signed()).or(Err("i64"))
136            }
137    }
138}
139
140fn parse_shell_literal_number(s: &str, radix: u64) -> Result<i64, &'static str> {
141    if !(2..=64).contains(&radix) {
142        return Err("invalid base");
143    }
144
145    // For bases <= 36: case-insensitive (a-z and A-Z both map to 10-35)
146    // For bases > 36 (bash extension):
147    //   0-9 = 0-9, a-z = 10-35, A-Z = 36-61, @ = 62, _ = 63
148    let mut result: i64 = 0;
149
150    for ch in s.chars() {
151        let digit_val = if radix <= 36 {
152            match ch {
153                '0'..='9' => (ch as u64) - ('0' as u64),
154                'a'..='z' => (ch as u64) - ('a' as u64) + 10,
155                'A'..='Z' => (ch as u64) - ('A' as u64) + 10,
156                _ => return Err("invalid digit"),
157            }
158        } else {
159            match ch {
160                '0'..='9' => (ch as u64) - ('0' as u64),
161                'a'..='z' => (ch as u64) - ('a' as u64) + 10,
162                'A'..='Z' => (ch as u64) - ('A' as u64) + 36,
163                '@' => 62,
164                '_' => 63,
165                _ => return Err("invalid digit"),
166            }
167        };
168
169        if digit_val >= radix {
170            return Err("value too great for base");
171        }
172
173        result = result
174            .wrapping_mul(radix.cast_signed())
175            .wrapping_add(digit_val.cast_signed());
176    }
177
178    Ok(result)
179}