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(
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
81pub(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 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 let expr = shrub_parser::arithmetic::parse(&expanded_self)
106 .map_err(|_e| EvalError::ParseError(expanded_self))?;
107
108 if trace_if_needed && shell.options().print_commands_and_arguments {
110 shell
111 .trace_command(params, std::format!("(( {expr} ))"))
112 .await;
113 }
114
115 expr.eval(shell)
117}
118
119pub trait Evaluatable {
121 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 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 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 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 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
411const 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}