Skip to main content

yash_arith/ast/
portability.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2026 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//! Items for verifying portability of parsed expressions
18
19use super::Ast;
20use super::PostfixOperator;
21use super::PrefixOperator;
22use std::ops::Range;
23use thiserror::Error;
24
25/// Cause of an error because an expression contains a non-portable construct
26#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
27#[non_exhaustive]
28pub enum PortabilityError {
29    /// An increment or decrement operator is used while the `portable` option
30    /// is on.
31    #[error("the increment and decrement operators are not portable")]
32    IncrementDecrement,
33}
34
35/// Description of a non-portable construct found in an expression
36#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
37#[error("{cause}")]
38pub struct Error {
39    /// Cause of the error
40    pub cause: PortabilityError,
41    /// Range of the non-portable construct in the parsed expression
42    pub location: Range<usize>,
43}
44
45/// Checks that the parsed expression contains no non-portable constructs.
46pub fn check(ast: &[Ast<'_>]) -> Result<(), Error> {
47    let location = ast
48        .iter()
49        .filter_map(|node| match node {
50            Ast::Prefix {
51                operator: PrefixOperator::Increment | PrefixOperator::Decrement,
52                location,
53            }
54            | Ast::Postfix {
55                operator: PostfixOperator::Increment | PostfixOperator::Decrement,
56                location,
57            } => Some(location),
58            _ => None,
59        })
60        .min_by_key(|location| location.start);
61
62    match location {
63        Some(location) => Err(Error {
64            cause: PortabilityError::IncrementDecrement,
65            location: location.clone(),
66        }),
67        None => Ok(()),
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::ast::parse;
75    use crate::token::PeekableTokens;
76
77    fn check_expression(expression: &str) -> Result<(), Error> {
78        let ast = parse(PeekableTokens::from(expression)).unwrap();
79        check(&ast)
80    }
81
82    #[test]
83    fn portable_expression() {
84        assert_eq!(check_expression("foo = -(1 + 2)"), Ok(()));
85    }
86
87    #[test]
88    fn prefix_increment_and_decrement() {
89        for (expression, location) in [("  ++foo", 2..4), ("--bar  ", 0..2)] {
90            assert_eq!(
91                check_expression(expression),
92                Err(Error {
93                    cause: PortabilityError::IncrementDecrement,
94                    location,
95                })
96            );
97        }
98    }
99
100    #[test]
101    fn postfix_increment_and_decrement() {
102        for (expression, location) in [("  foo++", 5..7), ("bar--  ", 3..5)] {
103            assert_eq!(
104                check_expression(expression),
105                Err(Error {
106                    cause: PortabilityError::IncrementDecrement,
107                    location,
108                })
109            );
110        }
111    }
112
113    #[test]
114    fn non_portable_operator_in_unevaluated_operand() {
115        assert_eq!(
116            check_expression("1 || foo++"),
117            Err(Error {
118                cause: PortabilityError::IncrementDecrement,
119                location: 8..10,
120            })
121        );
122    }
123
124    #[test]
125    fn first_non_portable_operator_in_source_order() {
126        // Prefix operators appear in the AST from the innermost to the
127        // outermost, which is the reverse of their order in the source.
128        assert_eq!(
129            check_expression("++--foo + bar++"),
130            Err(Error {
131                cause: PortabilityError::IncrementDecrement,
132                location: 0..2,
133            })
134        );
135    }
136}