//! Parser for shell arithmetic expressions. use crate::ast; use crate::error; /// Parses a shell arithmetic expression. /// /// # Arguments /// /// * `input` - The arithmetic expression to parse, in string form. pub fn parse(input: &str) -> Result { cacheable_parse(input) } #[cached::macros::cached(max_size = 64, key = "String", convert = r#"{ input.to_owned() }"#)] fn cacheable_parse(input: &str) -> Result { tracing::debug!(target: "arithmetic", "parsing arithmetic expression: '{input}'"); arithmetic::full_expression(input) .map_err(|e| error::WordParseError::ArithmeticExpression(e.into())) } peg::parser! { grammar arithmetic() for str { // An expression holding nothing but whitespace evaluates to 0, wherever it came from: // `$(( ))`, `let ''`, or a blank array subscript. pub(crate) rule full_expression() -> ast::ArithmeticExpr = _ ![_] { ast::ArithmeticExpr::Literal(0) } / _ e:expression() _ { e } pub(crate) rule expression() -> ast::ArithmeticExpr = precedence!{ x:(@) _ "," _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Comma, Box::new(x), Box::new(y)) } -- x:lvalue() _ "*=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Multiply, x, Box::new(y)) } x:lvalue() _ "/=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Divide, x, Box::new(y)) } x:lvalue() _ "%=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Modulo, x, Box::new(y)) } x:lvalue() _ "+=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Add, x, Box::new(y)) } x:lvalue() _ "-=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Subtract, x, Box::new(y)) } x:lvalue() _ "<<=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftLeft, x, Box::new(y)) } x:lvalue() _ ">>=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftRight, x, Box::new(y)) } x:lvalue() _ "&=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseAnd, x, Box::new(y)) } x:lvalue() _ "|=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseOr, x, Box::new(y)) } x:lvalue() _ "^=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseXor, x, Box::new(y)) } x:lvalue() _ "=" _ y:(@) { ast::ArithmeticExpr::Assignment(x, Box::new(y)) } -- x:@ _ "?" _ y:expression() _ ":" _ z:(@) { ast::ArithmeticExpr::Conditional(Box::new(x), Box::new(y), Box::new(z)) } -- x:(@) _ "||" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalOr, Box::new(x), Box::new(y)) } -- x:(@) _ "&&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalAnd, Box::new(x), Box::new(y)) } -- x:(@) _ "|" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseOr, Box::new(x), Box::new(y)) } -- x:(@) _ "^" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseXor, Box::new(x), Box::new(y)) } -- x:(@) _ "&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseAnd, Box::new(x), Box::new(y)) } -- x:(@) _ "==" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Equals, Box::new(x), Box::new(y)) } x:(@) _ "!=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::NotEquals, Box::new(x), Box::new(y)) } -- x:(@) _ "<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThan, Box::new(x), Box::new(y)) } x:(@) _ ">" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThan, Box::new(x), Box::new(y)) } x:(@) _ "<=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThanOrEqualTo, Box::new(x), Box::new(y)) } x:(@) _ ">=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThanOrEqualTo, Box::new(x), Box::new(y)) } -- x:(@) _ "<<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftLeft, Box::new(x), Box::new(y)) } x:(@) _ ">>" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftRight, Box::new(x), Box::new(y)) } -- x:(@) _ "+" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Add, Box::new(x), Box::new(y)) } x:(@) _ "-" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Subtract, Box::new(x), Box::new(y)) } -- x:(@) _ "*" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Multiply, Box::new(x), Box::new(y)) } x:(@) _ "%" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Modulo, Box::new(x), Box::new(y)) } x:(@) _ "/" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Divide, Box::new(x), Box::new(y)) } -- x:@ _ "**" _ y:(@) { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Power, Box::new(x), Box::new(y)) } -- "!" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::LogicalNot, Box::new(x)) } "~" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::BitwiseNot, Box::new(x)) } -- // NOTE: We add negative lookahead to avoid ambiguity with the pre-increment/pre-decrement operators. "+" !['+'] _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryPlus, Box::new(x)) } "-" !['-'] _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryMinus, Box::new(x)) } -- "++" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixIncrement, x) } "--" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixDecrement, x) } -- x:lvalue() _ "++" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixIncrement, x) } x:lvalue() _ "--" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixDecrement, x) } -- n:literal_number() { ast::ArithmeticExpr::Literal(n) } l:lvalue() { ast::ArithmeticExpr::Reference(l) } "(" _ expr:expression() _ ")" { expr } } rule lvalue() -> ast::ArithmeticTarget = name:variable_name() "[" index:expression() "]" { ast::ArithmeticTarget::ArrayElement(name.to_owned(), Box::new(index)) } / name:variable_name() { ast::ArithmeticTarget::Variable(name.to_owned()) } rule variable_name() -> &'input str = $(['a'..='z' | 'A'..='Z' | '_'](['a'..='z' | 'A'..='Z' | '_' | '0'..='9']*)) rule _() -> () = quiet!{[' ' | '\t' | '\n' | '\r']*} {} rule literal_number() -> i64 = // Literal with explicit radix (format: #) radix:decimal_literal() "#" s:$(['0'..='9' | 'a'..='z' | 'A'..='Z' | '@' | '_']+) {? parse_shell_literal_number(s, radix.cast_unsigned()) } / // Hex literal "0" ['x' | 'X'] s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*) {? i64::from_str_radix(s, 16).or(Err("i64")) } / // Octal literal s:$("0" ['0'..='8']*) {? i64::from_str_radix(s, 8).or(Err("i64")) } / // Decimal literal decimal_literal() rule decimal_literal() -> i64 = s:$(['1'..='9'] ['0'..='9']*) {? // Parse as u64 first, then cast to i64. This handles values like // 9223372036854775808 (i64::MAX + 1) which is needed for INT64_MIN // when preceded by unary minus: -(9223372036854775808) wraps to i64::MIN. s.parse::().map(|v| v.cast_signed()).or(Err("i64")) } } } fn parse_shell_literal_number(s: &str, radix: u64) -> Result { if !(2..=64).contains(&radix) { return Err("invalid base"); } // For bases <= 36: case-insensitive (a-z and A-Z both map to 10-35) // For bases > 36 (bash extension): // 0-9 = 0-9, a-z = 10-35, A-Z = 36-61, @ = 62, _ = 63 let mut result: i64 = 0; for ch in s.chars() { let digit_val = if radix <= 36 { match ch { '0'..='9' => (ch as u64) - ('0' as u64), 'a'..='z' => (ch as u64) - ('a' as u64) + 10, 'A'..='Z' => (ch as u64) - ('A' as u64) + 10, _ => return Err("invalid digit"), } } else { match ch { '0'..='9' => (ch as u64) - ('0' as u64), 'a'..='z' => (ch as u64) - ('a' as u64) + 10, 'A'..='Z' => (ch as u64) - ('A' as u64) + 36, '@' => 62, '_' => 63, _ => return Err("invalid digit"), } }; if digit_val >= radix { return Err("value too great for base"); } result = result .wrapping_mul(radix.cast_signed()) .wrapping_add(digit_val.cast_signed()); } Ok(result) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_blank_expression_yields_zero() { for input in ["", " ", "\t", " \n "] { assert!( matches!(parse(input), Ok(ast::ArithmeticExpr::Literal(0))), "expected 0 for {input:?}" ); } } #[test] fn parse_rejects_an_incomplete_expression() { assert!(parse("1 +").is_err()); } }