1use std::borrow::Cow;
4
5use shrub_parser::ast;
6
7use crate::{ExecutionParameters, Shell, env, expansion, extensions, variables};
8
9const MAX_VARIABLE_DEREF_DEPTH: u32 = 1024;
12
13#[derive(Debug, thiserror::Error)]
16pub enum EvalError {
17 #[error("division by zero")]
19 DivideByZero,
20
21 #[error("exponent less than 0")]
23 NegativeExponent,
24
25 #[error("failed to tokenize expression")]
27 FailedToTokenizeExpression,
28
29 #[error("failed to expand expression: {0}")]
31 FailedToExpandExpression(String),
32
33 #[error("failed to access array")]
35 FailedToAccessArray,
36
37 #[error("failed to update environment")]
39 FailedToUpdateEnvironment,
40
41 #[error("failed to parse expression: {0}")]
43 ParseError(String),
44
45 #[error("expanding unset variable: {0}")]
47 ExpandingUnsetVariable(String),
48
49 #[error("expression recursion level exceeded")]
51 RecursionLimitExceeded,
52}
53
54pub(crate) trait ExpandAndEvaluate {
56 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
90pub(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 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 let expr = shrub_parser::arithmetic::parse(&expanded_self)
115 .map_err(|_e| EvalError::ParseError(expanded_self))?;
116
117 if trace_if_needed && shell.options().print_commands_and_arguments {
119 shell
120 .trace_command(params, std::format!("(( {expr} ))"))
121 .await;
122 }
123
124 expr.eval(shell)
126}
127
128pub trait Evaluatable {
130 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 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 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 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 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
432const 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}