Skip to main content

clash_brush_core/
arithmetic.rs

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