yash_semantics/expansion/initial/
arith.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2022 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Arithmetic expansion
18
19use 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/// Types of errors that may occur in arithmetic expansion
40///
41/// This enum is essentially equivalent to `yash_arith::ErrorCause`. The
42/// differences between the two are:
43///
44/// - `ArithError` defines all error variants flatly while `ErrorCause` has
45///   nested variants.
46/// - `ArithError` may contain informative [`Location`] that can be used to
47///   produce an error message with annotated code while `ErrorCause` may just
48///   specify a location as an index range.
49#[derive(Clone, Debug, Eq, Error, PartialEq)]
50pub enum ArithError {
51    /// A value token contains an invalid character.
52    #[error("invalid numeric constant")]
53    InvalidNumericConstant,
54
55    /// An expression contains a character that is not a whitespace, number, or
56    /// number.
57    #[error("invalid character")]
58    InvalidCharacter,
59
60    /// Expression with a missing value
61    #[error("incomplete expression")]
62    IncompleteExpression,
63
64    /// Operator missing
65    #[error("expected an operator")]
66    MissingOperator,
67
68    /// `(` without `)`
69    #[error("unmatched parenthesis")]
70    UnclosedParenthesis { opening_location: Location },
71
72    /// `?` without `:`
73    #[error("`?` without matching `:`")]
74    QuestionWithoutColon { question_location: Location },
75
76    /// `:` without `?`
77    #[error("`:` without matching `?`")]
78    ColonWithoutQuestion,
79
80    /// Other error in operator usage
81    #[error("invalid use of operator")]
82    InvalidOperator,
83
84    /// A variable value that is not a valid number
85    #[error("invalid variable value: {0:?}")]
86    InvalidVariableValue(String),
87
88    /// Result out of bounds
89    #[error("overflow")]
90    Overflow,
91
92    /// Division by zero
93    #[error("division by zero")]
94    DivisionByZero,
95
96    /// Left bit-shifting of a negative value
97    #[error("left-shifting a negative integer")]
98    LeftShiftingNegative,
99
100    /// Bit-shifting with a negative right-hand-side operand
101    #[error("negative shift width")]
102    ReverseShifting,
103
104    /// Assignment with a left-hand-side operand not being a variable
105    #[error("assignment to a non-variable")]
106    AssignmentToValue,
107}
108
109impl ArithError {
110    /// Returns a location related with this error and a message describing the
111    /// location.
112    #[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/// Error expanding an unset variable
137#[derive(Clone, Debug, Eq, PartialEq)]
138struct UnsetVariable {
139    param: Param,
140}
141
142/// Converts `yash_arith::ErrorCause` into `initial::ErrorCause`.
143///
144/// The `source` argument must be the arithmetic expression being expanded.
145/// It is used to reproduce a location contained in the error cause.
146#[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                // TODO If the variable exists but is not scalar, UnsetVariable
219                // does not seem to be the right error.
220                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}