Skip to main content

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::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/// Types of errors that may occur in arithmetic expansion
41///
42/// This enum is essentially equivalent to `yash_arith::ErrorCause`. The
43/// differences between the two are:
44///
45/// - `ArithError` defines all error variants flatly while `ErrorCause` has
46///   nested variants.
47/// - `ArithError` may contain informative [`Location`] that can be used to
48///   produce an error message with annotated code while `ErrorCause` may just
49///   specify a location as an index range.
50#[derive(Clone, Debug, Eq, Error, PartialEq)]
51pub enum ArithError {
52    /// A value token contains an invalid character.
53    #[error("invalid numeric constant")]
54    InvalidNumericConstant,
55
56    /// An expression contains a character that is not a whitespace, number, or
57    /// number.
58    #[error("invalid character")]
59    InvalidCharacter,
60
61    /// Expression with a missing value
62    #[error("incomplete expression")]
63    IncompleteExpression,
64
65    /// Operator missing
66    #[error("expected an operator")]
67    MissingOperator,
68
69    /// `(` without `)`
70    #[error("unmatched parenthesis")]
71    UnclosedParenthesis { opening_location: Location },
72
73    /// `?` without `:`
74    #[error("`?` without matching `:`")]
75    QuestionWithoutColon { question_location: Location },
76
77    /// `:` without `?`
78    #[error("`:` without matching `?`")]
79    ColonWithoutQuestion,
80
81    /// Other error in operator usage
82    #[error("invalid use of operator")]
83    InvalidOperator,
84
85    /// A variable value that is not a valid number
86    #[error("invalid variable value: {0:?}")]
87    InvalidVariableValue(String),
88
89    /// Result out of bounds
90    #[error("overflow")]
91    Overflow,
92
93    /// Division by zero
94    #[error("division by zero")]
95    DivisionByZero,
96
97    /// Left bit-shifting of a negative value
98    #[error("left-shifting a negative integer")]
99    LeftShiftingNegative,
100
101    /// Bit-shifting with a negative right-hand-side operand
102    #[error("negative shift width")]
103    ReverseShifting,
104
105    /// Assignment with a left-hand-side operand not being a variable
106    #[error("assignment to a non-variable")]
107    AssignmentToValue,
108}
109
110impl ArithError {
111    /// Returns a location related with this error and a message describing the
112    /// location.
113    #[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/// Error expanding an unset variable
138#[derive(Clone, Debug, Eq, PartialEq)]
139struct UnsetVariable {
140    param: Param,
141}
142
143/// Converts `yash_arith::ErrorCause` into `initial::ErrorCause`.
144///
145/// The `source` argument must be the arithmetic expression being expanded.
146/// It is used to reproduce a location contained in the error cause.
147#[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                // TODO If the variable exists but is not scalar, UnsetVariable
220                // does not seem to be the right error.
221                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}