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