1use std::borrow::Cow;
4
5use crate::{ExecutionParameters, Shell, env, expansion, extensions, variables};
6use brush_parser::ast;
7
8#[derive(Debug, thiserror::Error)]
10pub enum EvalError {
11 #[error("division by zero")]
13 DivideByZero,
14
15 #[error("exponent less than 0")]
17 NegativeExponent,
18
19 #[error("failed to tokenize expression")]
21 FailedToTokenizeExpression,
22
23 #[error("failed to expand expression: {0}")]
25 FailedToExpandExpression(String),
26
27 #[error("failed to access array")]
29 FailedToAccessArray,
30
31 #[error("failed to update environment")]
33 FailedToUpdateEnvironment,
34
35 #[error("failed to parse expression: {0}")]
37 ParseError(String),
38
39 #[error("expanding unset variable: {0}")]
41 ExpandingUnsetVariable(String),
42}
43
44pub(crate) trait ExpandAndEvaluate {
46 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
71pub(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 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 let expr = brush_parser::arithmetic::parse(&expanded_self)
95 .map_err(|_e| EvalError::ParseError(expanded_self))?;
96
97 if trace_if_needed && shell.options().print_commands_and_arguments {
99 shell
100 .trace_command(params, std::format!("(( {expr} ))"))
101 .await;
102 }
103
104 expr.eval(shell)
106}
107
108pub trait Evaluatable {
110 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 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 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 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
364const 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}