Skip to main content

shrub_core/
arithmetic.rs

1//! Arithmetic evaluation
2
3use std::borrow::Cow;
4
5use shrub_parser::ast;
6
7use crate::{ExecutionParameters, Shell, env, expansion, extensions, variables};
8
9/// Maximum recursion depth for arithmetic variable dereference chains
10/// (e.g., a=b, b=c, c=a would cycle through variable dereferences).
11const MAX_VARIABLE_DEREF_DEPTH: u32 = 1024;
12
13/// Represents an error that occurs during evaluation of an arithmetic
14/// expression.
15#[derive(Debug, thiserror::Error)]
16pub enum EvalError {
17    /// Division by zero.
18    #[error("division by zero")]
19    DivideByZero,
20
21    /// Negative exponent.
22    #[error("exponent less than 0")]
23    NegativeExponent,
24
25    /// Failed to tokenize an arithmetic expression.
26    #[error("failed to tokenize expression")]
27    FailedToTokenizeExpression,
28
29    /// Failed to expand an arithmetic expression.
30    #[error("failed to expand expression: {0}")]
31    FailedToExpandExpression(String),
32
33    /// Failed to access an element of an array.
34    #[error("failed to access array")]
35    FailedToAccessArray,
36
37    /// Failed to update the shell environment in an assignment operator.
38    #[error("failed to update environment")]
39    FailedToUpdateEnvironment,
40
41    /// Failed to parse an arithmetic expression.
42    #[error("failed to parse expression: {0}")]
43    ParseError(String),
44
45    /// Error expanding an unset variable.
46    #[error("expanding unset variable: {0}")]
47    ExpandingUnsetVariable(String),
48
49    /// Expression recursion level exceeded.
50    #[error("expression recursion level exceeded")]
51    RecursionLimitExceeded,
52}
53
54/// Trait implemented by arithmetic expressions that can be evaluated.
55pub(crate) trait ExpandAndEvaluate {
56    /// Evaluate the given expression, returning the resulting numeric value.
57    ///
58    /// # Arguments
59    ///
60    /// * `shell` - The shell to use for evaluation.
61    /// * `trace_if_needed` - Whether to trace the evaluation.
62    async fn eval(
63        &self,
64        shell: &mut Shell<impl extensions::ShellExtensions>,
65        params: &ExecutionParameters,
66        trace_if_needed: bool,
67    ) -> Result<i64, EvalError>;
68}
69
70impl ExpandAndEvaluate for ast::UnexpandedArithmeticExpr {
71    async fn eval(
72        &self,
73        shell: &mut Shell<impl extensions::ShellExtensions>,
74        params: &ExecutionParameters,
75        trace_if_needed: bool,
76    ) -> Result<i64, EvalError> {
77        expand_and_eval(shell, params, self.value.as_str(), trace_if_needed).await
78    }
79}
80
81/// Evaluate the given arithmetic expression, returning the resulting numeric
82/// value.
83///
84/// # Arguments
85///
86/// * `shell` - The shell to use for evaluation.
87/// * `expr` - The unexpanded arithmetic expression to evaluate.
88/// * `trace_if_needed` - Whether to trace the evaluation.
89pub(crate) async fn expand_and_eval(
90    shell: &mut Shell<impl extensions::ShellExtensions>,
91    params: &ExecutionParameters,
92    expr: &str,
93    trace_if_needed: bool,
94) -> Result<i64, EvalError> {
95    // Per documentation, first shell-expand it.
96    let options = expansion::ExpanderOptions {
97        tilde_expand: false,
98        ..Default::default()
99    };
100    let expanded_self = expansion::basic_expand_word_with_options(shell, params, expr, &options)
101        .await
102        .map_err(|_e| EvalError::FailedToExpandExpression(expr.to_owned()))?;
103
104    // Now parse.
105    let expr = shrub_parser::arithmetic::parse(&expanded_self)
106        .map_err(|_e| EvalError::ParseError(expanded_self))?;
107
108    // Trace if applicable.
109    if trace_if_needed && shell.options().print_commands_and_arguments {
110        shell
111            .trace_command(params, std::format!("(( {expr} ))"))
112            .await;
113    }
114
115    // Now evaluate.
116    expr.eval(shell)
117}
118
119/// Trait implemented by evaluatable arithmetic expressions.
120pub trait Evaluatable {
121    /// Evaluate the given arithmetic expression, returning the resulting
122    /// numeric value.
123    ///
124    /// # Arguments
125    ///
126    /// * `shell` - The shell to use for evaluation.
127    fn eval(&self, shell: &mut Shell<impl extensions::ShellExtensions>) -> Result<i64, EvalError>;
128}
129
130impl Evaluatable for ast::ArithmeticExpr {
131    fn eval(&self, shell: &mut Shell<impl extensions::ShellExtensions>) -> Result<i64, EvalError> {
132        eval_expr_impl(self, shell, 0)
133    }
134}
135
136fn eval_expr_impl(
137    expr: &ast::ArithmeticExpr,
138    shell: &mut Shell<impl extensions::ShellExtensions>,
139    depth: u32,
140) -> Result<i64, EvalError> {
141    let value = match expr {
142        ast::ArithmeticExpr::Literal(l) => *l,
143        ast::ArithmeticExpr::Reference(lvalue) => deref_lvalue(shell, lvalue, depth)?,
144        ast::ArithmeticExpr::UnaryOp(op, operand) => apply_unary_op(shell, *op, operand, depth)?,
145        ast::ArithmeticExpr::BinaryOp(op, left, right) => {
146            apply_binary_op(shell, *op, left, right, depth)?
147        }
148        ast::ArithmeticExpr::Conditional(condition, then_expr, else_expr) => {
149            let conditional_eval = eval_expr_impl(condition, shell, depth)?;
150
151            // Ensure we only evaluate the branch indicated by the condition.
152            if conditional_eval != 0 {
153                eval_expr_impl(then_expr, shell, depth)?
154            } else {
155                eval_expr_impl(else_expr, shell, depth)?
156            }
157        }
158        ast::ArithmeticExpr::Assignment(lvalue, rhs) => {
159            let expr_eval = eval_expr_impl(rhs, shell, depth)?;
160            assign(shell, lvalue, expr_eval, depth)?
161        }
162        ast::ArithmeticExpr::UnaryAssignment(op, lvalue) => {
163            apply_unary_assignment_op(shell, lvalue, *op, depth)?
164        }
165        ast::ArithmeticExpr::BinaryAssignment(op, lvalue, operand) => {
166            let value = apply_binary_op(
167                shell,
168                *op,
169                &ast::ArithmeticExpr::Reference(lvalue.clone()),
170                operand,
171                depth,
172            )?;
173            assign(shell, lvalue, value, depth)?
174        }
175    };
176
177    Ok(value)
178}
179
180fn get_var_value<'a>(
181    shell: &'a Shell<impl extensions::ShellExtensions>,
182    name: &str,
183) -> Result<Cow<'a, str>, EvalError> {
184    let value = shell.env_var(name).map(|var| var.resolve_value(shell));
185
186    if let Some(value) = value
187        && value.is_set()
188    {
189        return Ok(value.to_cow_str(shell).to_string().into());
190    }
191
192    if shell.options().treat_unset_variables_as_error {
193        return Err(EvalError::ExpandingUnsetVariable(name.into()));
194    }
195
196    Ok("".into())
197}
198
199fn deref_lvalue(
200    shell: &mut Shell<impl extensions::ShellExtensions>,
201    lvalue: &ast::ArithmeticTarget,
202    depth: u32,
203) -> Result<i64, EvalError> {
204    let value_str: Cow<'_, str> = match lvalue {
205        ast::ArithmeticTarget::Variable(name) => get_var_value(shell, name.as_str())?,
206        ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
207            let index_str = eval_expr_impl(index_expr, shell, depth)?.to_string();
208
209            shell
210                .env()
211                .get(name)
212                .map_or_else(
213                    || Ok(None),
214                    |(_, v)| v.value().get_at(index_str.as_str(), shell),
215                )
216                .map_err(|_err| EvalError::FailedToAccessArray)?
217                .unwrap_or(Cow::Borrowed(""))
218        }
219    };
220
221    let parsed_value = shrub_parser::arithmetic::parse(value_str.as_ref())
222        .map_err(|_err| EvalError::ParseError(value_str.to_string()))?;
223
224    // Literals don't need depth tracking — they can't cause recursion.
225    // Only increment depth when the parsed value requires further evaluation
226    // (i.e., it references other variables), matching bash's behavior.
227    if matches!(parsed_value, ast::ArithmeticExpr::Literal(_)) {
228        return eval_expr_impl(&parsed_value, shell, depth);
229    }
230
231    let new_depth = depth + 1;
232    if new_depth > MAX_VARIABLE_DEREF_DEPTH {
233        return Err(EvalError::RecursionLimitExceeded);
234    }
235
236    eval_expr_impl(&parsed_value, shell, new_depth)
237}
238
239fn apply_unary_op(
240    shell: &mut Shell<impl extensions::ShellExtensions>,
241    op: ast::UnaryOperator,
242    operand: &ast::ArithmeticExpr,
243    depth: u32,
244) -> Result<i64, EvalError> {
245    let operand_eval = eval_expr_impl(operand, shell, depth)?;
246
247    match op {
248        ast::UnaryOperator::UnaryPlus => Ok(operand_eval),
249        ast::UnaryOperator::UnaryMinus => Ok(operand_eval.wrapping_neg()),
250        ast::UnaryOperator::BitwiseNot => Ok(!operand_eval),
251        ast::UnaryOperator::LogicalNot => Ok(bool_to_i64(operand_eval == 0)),
252    }
253}
254
255fn apply_binary_op(
256    shell: &mut Shell<impl extensions::ShellExtensions>,
257    op: ast::BinaryOperator,
258    left: &ast::ArithmeticExpr,
259    right: &ast::ArithmeticExpr,
260    depth: u32,
261) -> Result<i64, EvalError> {
262    // First, special-case short-circuiting operators. For those, we need
263    // to ensure we don't eagerly evaluate both operands. After we
264    // get these out of the way, we can easily just evaluate operands
265    // for the other operators.
266    match op {
267        ast::BinaryOperator::LogicalAnd => {
268            let left = eval_expr_impl(left, shell, depth)?;
269            if left == 0 {
270                return Ok(bool_to_i64(false));
271            }
272
273            let right = eval_expr_impl(right, shell, depth)?;
274            return Ok(bool_to_i64(right != 0));
275        }
276        ast::BinaryOperator::LogicalOr => {
277            let left = eval_expr_impl(left, shell, depth)?;
278            if left != 0 {
279                return Ok(bool_to_i64(true));
280            }
281
282            let right = eval_expr_impl(right, shell, depth)?;
283            return Ok(bool_to_i64(right != 0));
284        }
285        _ => (),
286    }
287
288    // The remaining operators unconditionally operate both operands.
289    let left = eval_expr_impl(left, shell, depth)?;
290    let right = eval_expr_impl(right, shell, depth)?;
291
292    #[expect(clippy::cast_possible_truncation)]
293    #[expect(clippy::cast_sign_loss)]
294    match op {
295        ast::BinaryOperator::Power => {
296            if right >= 0 {
297                Ok(wrapping_pow_u64(left, right as u64))
298            } else {
299                Err(EvalError::NegativeExponent)
300            }
301        }
302        ast::BinaryOperator::Multiply => Ok(left.wrapping_mul(right)),
303        ast::BinaryOperator::Divide => {
304            if right == 0 {
305                Err(EvalError::DivideByZero)
306            } else {
307                Ok(left.wrapping_div(right))
308            }
309        }
310        ast::BinaryOperator::Modulo => {
311            if right == 0 {
312                Err(EvalError::DivideByZero)
313            } else {
314                Ok(left.wrapping_rem(right))
315            }
316        }
317        ast::BinaryOperator::Comma => Ok(right),
318        ast::BinaryOperator::Add => Ok(left.wrapping_add(right)),
319        ast::BinaryOperator::Subtract => Ok(left.wrapping_sub(right)),
320        ast::BinaryOperator::ShiftLeft => Ok(left.wrapping_shl(right as u32)),
321        ast::BinaryOperator::ShiftRight => Ok(left.wrapping_shr(right as u32)),
322        ast::BinaryOperator::LessThan => Ok(bool_to_i64(left < right)),
323        ast::BinaryOperator::LessThanOrEqualTo => Ok(bool_to_i64(left <= right)),
324        ast::BinaryOperator::GreaterThan => Ok(bool_to_i64(left > right)),
325        ast::BinaryOperator::GreaterThanOrEqualTo => Ok(bool_to_i64(left >= right)),
326        ast::BinaryOperator::Equals => Ok(bool_to_i64(left == right)),
327        ast::BinaryOperator::NotEquals => Ok(bool_to_i64(left != right)),
328        ast::BinaryOperator::BitwiseAnd => Ok(left & right),
329        ast::BinaryOperator::BitwiseXor => Ok(left ^ right),
330        ast::BinaryOperator::BitwiseOr => Ok(left | right),
331        ast::BinaryOperator::LogicalAnd => unreachable!("LogicalAnd covered above"),
332        ast::BinaryOperator::LogicalOr => unreachable!("LogicalOr covered above"),
333    }
334}
335
336fn apply_unary_assignment_op(
337    shell: &mut Shell<impl extensions::ShellExtensions>,
338    lvalue: &ast::ArithmeticTarget,
339    op: ast::UnaryAssignmentOperator,
340    depth: u32,
341) -> Result<i64, EvalError> {
342    let value = deref_lvalue(shell, lvalue, depth)?;
343
344    match op {
345        ast::UnaryAssignmentOperator::PrefixIncrement => {
346            let new_value = value.wrapping_add(1);
347            assign(shell, lvalue, new_value, depth)?;
348            Ok(new_value)
349        }
350        ast::UnaryAssignmentOperator::PrefixDecrement => {
351            let new_value = value.wrapping_sub(1);
352            assign(shell, lvalue, new_value, depth)?;
353            Ok(new_value)
354        }
355        ast::UnaryAssignmentOperator::PostfixIncrement => {
356            let new_value = value.wrapping_add(1);
357            assign(shell, lvalue, new_value, depth)?;
358            Ok(value)
359        }
360        ast::UnaryAssignmentOperator::PostfixDecrement => {
361            let new_value = value.wrapping_sub(1);
362            assign(shell, lvalue, new_value, depth)?;
363            Ok(value)
364        }
365    }
366}
367
368fn assign(
369    shell: &mut Shell<impl extensions::ShellExtensions>,
370    lvalue: &ast::ArithmeticTarget,
371    value: i64,
372    depth: u32,
373) -> Result<i64, EvalError> {
374    match lvalue {
375        ast::ArithmeticTarget::Variable(name) => {
376            shell
377                .env_mut()
378                .update_or_add(
379                    name.as_str(),
380                    variables::ShellValueLiteral::Scalar(value.to_string()),
381                    |_| Ok(()),
382                    env::EnvironmentLookup::Anywhere,
383                    env::EnvironmentScope::Global,
384                )
385                .map_err(|_err| EvalError::FailedToUpdateEnvironment)?;
386        }
387        ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
388            let index_str = eval_expr_impl(index_expr, shell, depth)?.to_string();
389
390            shell
391                .env_mut()
392                .update_or_add_array_element(
393                    name.as_str(),
394                    index_str,
395                    value.to_string(),
396                    |_| Ok(()),
397                    env::EnvironmentLookup::Anywhere,
398                    env::EnvironmentScope::Global,
399                )
400                .map_err(|_err| EvalError::FailedToUpdateEnvironment)?;
401        }
402    }
403
404    Ok(value)
405}
406
407const fn bool_to_i64(value: bool) -> i64 {
408    if value { 1 } else { 0 }
409}
410
411// N.B. We implement our own version of wrapping_pow that takes a 64-bit
412// exponent. This seems to be the best way to guarantee that we handle overflow
413// cases with exponents correctly.
414const fn wrapping_pow_u64(mut base: i64, mut exponent: u64) -> i64 {
415    let mut result: i64 = 1;
416
417    while exponent > 0 {
418        if exponent % 2 == 1 {
419            result = result.wrapping_mul(base);
420        }
421
422        base = base.wrapping_mul(base);
423        exponent /= 2;
424    }
425
426    result
427}