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