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