1use super::super::ErrorCause;
20use super::super::attr::AttrChar;
21use super::super::attr::Origin;
22use super::super::phrase::Phrase;
23use super::Env;
24use super::Error;
25use crate::Runtime;
26use crate::expansion::AssignReadOnlyError;
27use crate::expansion::expand_text;
28use std::ops::Range;
29use std::rc::Rc;
30use yash_arith::Config;
31use yash_arith::eval_with_config;
32use yash_env::option::Option::{Portable, Unset};
33use yash_env::option::State::{Off, On};
34use yash_env::variable::Scope::Global;
35use yash_syntax::source::Code;
36use yash_syntax::source::Location;
37use yash_syntax::source::Source;
38use yash_syntax::syntax::Param;
39use yash_syntax::syntax::Text;
40
41#[derive(Clone, Debug, Eq, Error, PartialEq)]
52#[non_exhaustive]
53pub enum ArithError {
54 #[error("invalid numeric constant")]
56 InvalidNumericConstant,
57
58 #[error("invalid character")]
61 InvalidCharacter,
62
63 #[error("incomplete expression")]
65 IncompleteExpression,
66
67 #[error("expected an operator")]
69 MissingOperator,
70
71 #[error("unmatched parenthesis")]
73 UnclosedParenthesis { opening_location: Location },
74
75 #[error("`?` without matching `:`")]
77 QuestionWithoutColon { question_location: Location },
78
79 #[error("`:` without matching `?`")]
81 ColonWithoutQuestion,
82
83 #[error("invalid use of operator")]
85 InvalidOperator,
86
87 #[error("the increment and decrement operators are not portable")]
90 NonPortableIncrementDecrement,
91
92 #[error("invalid variable value: {0:?}")]
94 InvalidVariableValue(String),
95
96 #[error("overflow")]
98 Overflow,
99
100 #[error("division by zero")]
102 DivisionByZero,
103
104 #[error("left-shifting a negative integer")]
106 LeftShiftingNegative,
107
108 #[error("negative shift width")]
110 ReverseShifting,
111
112 #[error("assignment to a non-variable")]
114 AssignmentToValue,
115
116 #[error("{0}")]
122 Unrecognized(String),
123}
124
125impl ArithError {
126 #[must_use]
129 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
130 use ArithError::*;
131 match self {
132 InvalidNumericConstant
133 | InvalidCharacter
134 | IncompleteExpression
135 | MissingOperator
136 | ColonWithoutQuestion
137 | InvalidOperator
138 | NonPortableIncrementDecrement
139 | InvalidVariableValue(_)
140 | Overflow
141 | DivisionByZero
142 | LeftShiftingNegative
143 | ReverseShifting
144 | AssignmentToValue
145 | Unrecognized(_) => None,
146 UnclosedParenthesis { opening_location } => {
147 Some((opening_location, "the opening parenthesis was here"))
148 }
149 QuestionWithoutColon { question_location } => Some((question_location, "`?` was here")),
150 }
151 }
152}
153
154#[derive(Clone, Debug, Eq, Error, PartialEq)]
156#[error("unset variable `{param}`")]
157struct UnsetVariable {
158 param: Param,
159}
160
161#[must_use]
166fn convert_error_cause(
167 cause: yash_arith::ErrorCause<UnsetVariable, AssignReadOnlyError>,
168 source: &Rc<Code>,
169) -> ErrorCause {
170 use ArithError::*;
171 use yash_arith::{
172 ErrorCause as EC, EvalError as EE, PortabilityError as PE, SyntaxError as SE,
173 TokenError as TE,
174 };
175 match cause {
176 EC::SyntaxError(SE::TokenError(TE::InvalidNumericConstant)) => {
177 ErrorCause::ArithError(InvalidNumericConstant)
178 }
179 EC::SyntaxError(SE::TokenError(TE::InvalidCharacter)) => {
180 ErrorCause::ArithError(InvalidCharacter)
181 }
182 EC::SyntaxError(SE::IncompleteExpression) => ErrorCause::ArithError(IncompleteExpression),
183 EC::SyntaxError(SE::MissingOperator) => ErrorCause::ArithError(MissingOperator),
184 EC::SyntaxError(SE::UnclosedParenthesis { opening_location }) => {
185 let opening_location = Location {
186 code: Rc::clone(source),
187 range: opening_location,
188 };
189 ErrorCause::ArithError(UnclosedParenthesis { opening_location })
190 }
191 EC::SyntaxError(SE::QuestionWithoutColon { question_location }) => {
192 let question_location = Location {
193 code: Rc::clone(source),
194 range: question_location,
195 };
196 ErrorCause::ArithError(QuestionWithoutColon { question_location })
197 }
198 EC::SyntaxError(SE::ColonWithoutQuestion) => ErrorCause::ArithError(ColonWithoutQuestion),
199 EC::SyntaxError(SE::InvalidOperator) => ErrorCause::ArithError(InvalidOperator),
200 EC::PortabilityError(PE::IncrementDecrement) => {
201 ErrorCause::ArithError(NonPortableIncrementDecrement)
202 }
203 EC::EvalError(EE::InvalidVariableValue(value)) => {
204 ErrorCause::ArithError(InvalidVariableValue(value))
205 }
206 EC::EvalError(EE::Overflow) => ErrorCause::ArithError(Overflow),
207 EC::EvalError(EE::DivisionByZero) => ErrorCause::ArithError(DivisionByZero),
208 EC::EvalError(EE::LeftShiftingNegative) => ErrorCause::ArithError(LeftShiftingNegative),
209 EC::EvalError(EE::ReverseShifting) => ErrorCause::ArithError(ReverseShifting),
210 EC::EvalError(EE::AssignmentToValue) => ErrorCause::ArithError(AssignmentToValue),
211 EC::EvalError(EE::GetVariableError(UnsetVariable { param })) => {
212 ErrorCause::UnsetParameter { param }
213 }
214 EC::EvalError(EE::AssignVariableError(e)) => ErrorCause::AssignReadOnly(e),
215 cause => ErrorCause::ArithError(Unrecognized(cause.to_string())),
216 }
217}
218
219struct VarEnv<'a, S> {
220 env: &'a mut yash_env::Env<S>,
221 expression: &'a str,
222 expansion_location: &'a Location,
223}
224
225impl<S> yash_arith::Env for VarEnv<'_, S> {
226 type GetVariableError = UnsetVariable;
227 type AssignVariableError = AssignReadOnlyError;
228
229 fn get_variable(&self, name: &str) -> Result<Option<&str>, UnsetVariable> {
230 match self.env.variables.get_scalar(name) {
231 Some(value) => Ok(Some(value)),
232 None => match self.env.options.get(Unset) {
233 Off => Err(UnsetVariable {
236 param: Param::variable(name),
237 }),
238 On => Ok(None),
239 },
240 }
241 }
242
243 fn assign_variable(
244 &mut self,
245 name: &str,
246 value: String,
247 range: Range<usize>,
248 ) -> Result<(), AssignReadOnlyError> {
249 let code = Rc::new(Code {
250 value: self.expression.to_string().into(),
251 start_line_number: 1.try_into().unwrap(),
252 source: Source::Arith {
253 original: self.expansion_location.clone(),
254 }
255 .into(),
256 });
257 self.env
258 .get_or_create_variable(name, Global)
259 .assign(value, Location { code, range })
260 .map(drop)
261 .map_err(|e| AssignReadOnlyError {
262 name: name.to_owned(),
263 new_value: e.new_value,
264 read_only_location: e.read_only_location,
265 vacancy: None,
266 })
267 }
268}
269
270pub async fn expand<S: Runtime + 'static>(
271 text: &Text,
272 location: &Location,
273 env: &mut Env<'_, S>,
274) -> Result<Phrase, Error> {
275 let (expression, exit_status) = expand_text(env.inner, text).await?;
276 if exit_status.is_some() {
277 env.last_command_subst_exit_status = exit_status;
278 }
279
280 let mut config = Config::new();
281 config.portable = env.inner.options.get(Portable) == On;
282 let result = eval_with_config(
283 &expression,
284 &mut VarEnv {
285 env: env.inner,
286 expression: &expression,
287 expansion_location: location,
288 },
289 config,
290 );
291
292 match result {
293 Ok(value) => {
294 let value = value.to_string();
295 let chars = value
296 .chars()
297 .map(|c| AttrChar {
298 value: c,
299 origin: Origin::SoftExpansion,
300 is_quoted: false,
301 is_quoting: false,
302 })
303 .collect();
304 Ok(Phrase::Field(chars))
305 }
306 Err(error) => {
307 let code = Rc::new(Code {
308 value: expression.into(),
309 start_line_number: 1.try_into().unwrap(),
310 source: Source::Arith {
311 original: location.clone(),
312 }
313 .into(),
314 });
315 let cause = convert_error_cause(error.cause, &code);
316 Err(Error {
317 cause,
318 location: Location {
319 code,
320 range: error.location,
321 },
322 })
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::tests::echo_builtin;
331 use crate::tests::return_builtin;
332 use futures_util::FutureExt as _;
333 use yash_env::semantics::ExitStatus;
334 use yash_env::system::Errno;
335 use yash_env::test_helper::in_virtual_system;
336 use yash_env::variable::Scope::Global;
337 use yash_env::variable::Value::Scalar;
338
339 #[test]
340 fn unrecognized_error() {
341 let error = ArithError::Unrecognized("new arithmetic error".into());
342
343 assert_eq!(error.to_string(), "new arithmetic error");
344 assert_eq!(error.related_location(), None);
345 }
346
347 #[test]
348 fn var_env_get_variable_success() {
349 use yash_arith::Env as _;
350 let mut env = yash_env::Env::new_virtual();
351 env.variables
352 .get_or_new("v", Global)
353 .assign("value", None)
354 .unwrap();
355 let location = Location::dummy("my location");
356 let env = VarEnv {
357 env: &mut env,
358 expression: "v",
359 expansion_location: &location,
360 };
361
362 let result = env.get_variable("v");
363 assert_eq!(result, Ok(Some("value")));
364 }
365
366 #[test]
367 fn var_env_get_variable_unset() {
368 use yash_arith::Env as _;
369 let mut env = yash_env::Env::new_virtual();
370 let location = Location::dummy("my location");
371 let env = VarEnv {
372 env: &mut env,
373 expression: "v",
374 expansion_location: &location,
375 };
376
377 let result = env.get_variable("v");
378 assert_eq!(result, Ok(None));
379 }
380
381 #[test]
382 fn var_env_get_variable_nounset() {
383 use yash_arith::Env as _;
384 let mut env = yash_env::Env::new_virtual();
385 env.options.set(Unset, Off);
386 let location = Location::dummy("my location");
387 let env = VarEnv {
388 env: &mut env,
389 expression: "0+v",
390 expansion_location: &location,
391 };
392
393 let result = env.get_variable("v");
394 assert_eq!(
395 result,
396 Err(UnsetVariable {
397 param: Param::variable("v")
398 })
399 );
400 }
401
402 #[test]
403 fn successful_inner_text_expansion() {
404 let text = "17%9".parse().unwrap();
405 let location = Location::dummy("my location");
406 let mut env = yash_env::Env::new_virtual();
407 let mut env = Env::new(&mut env);
408 let result = expand(&text, &location, &mut env).now_or_never().unwrap();
409 let c = AttrChar {
410 value: '8',
411 origin: Origin::SoftExpansion,
412 is_quoted: false,
413 is_quoting: false,
414 };
415 assert_eq!(result, Ok(Phrase::Char(c)));
416 assert_eq!(env.last_command_subst_exit_status, None);
417 }
418
419 #[test]
420 fn increment_decrement_allowed_without_portable_option() {
421 let text = "foo++".parse().unwrap();
422 let location = Location::dummy("my location");
423 let mut env = yash_env::Env::new_virtual();
424 let mut env2 = Env::new(&mut env);
425
426 let result = expand(&text, &location, &mut env2).now_or_never().unwrap();
427
428 assert!(result.is_ok());
429 assert_eq!(env.variables.get_scalar("foo"), Some("1"));
430 }
431
432 #[test]
433 fn increment_decrement_rejected_with_portable_option() {
434 let text = "foo++".parse().unwrap();
435 let location = Location::dummy("my location");
436 let mut env = yash_env::Env::new_virtual();
437 env.options.set(Portable, On);
438 let mut env = Env::new(&mut env);
439
440 let result = expand(&text, &location, &mut env).now_or_never().unwrap();
441
442 let error = result.unwrap_err();
443 assert_eq!(
444 error.cause,
445 ErrorCause::ArithError(ArithError::NonPortableIncrementDecrement)
446 );
447 assert_eq!(*error.location.code.value.borrow(), "foo++");
448 assert_eq!(error.location.range, 3..5);
449 }
450
451 #[test]
452 fn non_zero_exit_status_from_inner_text_expansion() {
453 in_virtual_system(|mut env, _state| async move {
454 let text = "$(echo 0; return -n 63)".parse().unwrap();
455 let location = Location::dummy("my location");
456 env.builtins.insert("echo", echo_builtin());
457 env.builtins.insert("return", return_builtin());
458 let mut env = Env::new(&mut env);
459 let result = expand(&text, &location, &mut env).await;
460 let c = AttrChar {
461 value: '0',
462 origin: Origin::SoftExpansion,
463 is_quoted: false,
464 is_quoting: false,
465 };
466 assert_eq!(result, Ok(Phrase::Char(c)));
467 assert_eq!(env.last_command_subst_exit_status, Some(ExitStatus(63)));
468 })
469 }
470
471 #[test]
472 fn exit_status_is_kept_if_inner_text_expansion_contains_no_command_substitution() {
473 let text = "0".parse().unwrap();
474 let location = Location::dummy("my location");
475 let mut env = yash_env::Env::new_virtual();
476 let mut env = Env::new(&mut env);
477 env.last_command_subst_exit_status = Some(ExitStatus(123));
478 let _ = expand(&text, &location, &mut env).now_or_never().unwrap();
479 assert_eq!(env.last_command_subst_exit_status, Some(ExitStatus(123)));
480 }
481
482 #[test]
483 fn error_in_inner_text_expansion() {
484 let text = "$(x)".parse().unwrap();
485 let location = Location::dummy("my location");
486 let mut env = yash_env::Env::new_virtual();
487 let mut env = Env::new(&mut env);
488 let result = expand(&text, &location, &mut env).now_or_never().unwrap();
489 let e = result.unwrap_err();
490 assert_eq!(e.cause, ErrorCause::CommandSubstError(Errno::ENOSYS));
491 assert_eq!(*e.location.code.value.borrow(), "$(x)");
492 assert_eq!(e.location.range, 0..4);
493 }
494
495 #[test]
496 fn variable_assigned_during_arithmetic_evaluation() {
497 let text = "3 + (x = 4 * 6)".parse().unwrap();
498 let location = Location::dummy("my location");
499 let mut env = yash_env::Env::new_virtual();
500 let mut env2 = Env::new(&mut env);
501 let _ = expand(&text, &location, &mut env2).now_or_never().unwrap();
502
503 let v = env.variables.get("x").unwrap();
504 assert_eq!(v.value, Some(Scalar("24".to_string())));
505 let location2 = v.last_assigned_location.as_ref().unwrap();
506 assert_eq!(*location2.code.value.borrow(), "3 + (x = 4 * 6)");
507 assert_eq!(location2.code.start_line_number.get(), 1);
508 assert_eq!(*location2.code.source, Source::Arith { original: location });
509 assert_eq!(location2.range, 5..6);
510 assert!(!v.is_exported);
511 assert_eq!(v.read_only_location, None);
512 }
513
514 #[test]
515 fn error_in_arithmetic_evaluation() {
516 let text = "09".parse().unwrap();
517 let location = Location::dummy("my location");
518 let mut env = yash_env::Env::new_virtual();
519 let mut env = Env::new(&mut env);
520 let result = expand(&text, &location, &mut env).now_or_never().unwrap();
521 let e = result.unwrap_err();
522 assert_eq!(
523 e.cause,
524 ErrorCause::ArithError(ArithError::InvalidNumericConstant)
525 );
526 assert_eq!(*e.location.code.value.borrow(), "09");
527 assert_eq!(
528 *e.location.code.source,
529 Source::Arith { original: location }
530 );
531 assert_eq!(e.location.range, 0..2);
532 }
533}