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