]> pintos-os.org Git - pspp/commitdiff
add expression parser
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 10 Sep 2026 18:57:01 +0000 (11:57 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 10 Sep 2026 18:57:01 +0000 (11:57 -0700)
rust/pspp/README.md
rust/pspp/src/spv/read/graph.rs
rust/pspp/src/spv/read/graph/var_set.rs [new file with mode: 0644]

index 31f478c009c6f493c783e51395c52ad982a12e61..558c9fd8e85c371d1cd43aa6af202beb8bf97baa 100644 (file)
@@ -14,7 +14,7 @@ This new version is very incomplete--in fact, only a subset of the
 
 To install this version of PSPP from source, first install Rust using
 the instructions at https://www.rust-lang.org/tools/install.  PSPP
-requires Rust 1.88 or later.  Once you've done that, you can download
+requires Rust 1.98 or later.  Once you've done that, you can download
 PSPP source code from `crates.io` and build and install it with:
 
 ```
index dbdea52f6e69ad8235e2c00bce15f3ec936c8d26..ed0411b7f995a5239236234ac841dd3a7f42d04f 100644 (file)
@@ -75,6 +75,7 @@ mod frames;
 mod patterns;
 mod style;
 mod symbols;
+mod var_set;
 
 #[derive(Clone, Debug, Default)]
 struct Map(HashMap<Datum<String>, String>);
diff --git a/rust/pspp/src/spv/read/graph/var_set.rs b/rust/pspp/src/spv/read/graph/var_set.rs
new file mode 100644 (file)
index 0000000..32f9333
--- /dev/null
@@ -0,0 +1,338 @@
+use std::{iter::Peekable, str::FromStr};
+
+use crate::spv::read::graph::Series;
+
+/// Operator precedence:
+///
+/// - `/` (highest)
+/// - `*`, `.` (middle)
+/// - `+` (lowest)
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum Operator {
+    /// `+`
+    Blend,
+    /// `*`
+    Cross,
+    /// `/`
+    Nest,
+    /// `.`
+    Dot,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum Expression {
+    Unity,
+    Variable(String),
+    Operation {
+        operator: Operator,
+        operands: Box<[Expression; 2]>,
+    },
+}
+
+impl Expression {
+    fn new_variable(name: impl Into<String>) -> Self {
+        Self::Variable(name.into())
+    }
+    fn new_operation(operator: Operator, operand1: Expression, operand2: Expression) -> Self {
+        Self::Operation {
+            operator,
+            operands: Box::new([operand1, operand2]),
+        }
+    }
+}
+
+/// Error parsing an [Expression].
+#[derive(Clone, Debug, thiserror::Error, displaydoc::Display, PartialEq, Eq)]
+enum ExpressionError {
+    /// Empty expression.
+    EmptyString,
+    /// Expression was following by garbage.
+    TrailingGarbage,
+    /// Expression missing right parenthesis.
+    ExpectedRightParen,
+    /// Syntax error parsing expression.
+    SyntaxError,
+    /// Unexpected end of expression.
+    UnexpectedEof,
+}
+
+impl FromStr for Expression {
+    type Err = ExpressionError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        #[derive(Clone, Debug, PartialEq, Eq)]
+        enum Token {
+            Operator(Operator),
+            LeftParen,
+            RightParen,
+            Semicolon,
+            Unity,
+            Id(String),
+        }
+        impl Token {
+            fn from_delim(c: char) -> Option<Self> {
+                match c {
+                    '+' => Some(Token::Operator(Operator::Blend)),
+                    '*' => Some(Token::Operator(Operator::Cross)),
+                    '/' => Some(Token::Operator(Operator::Nest)),
+                    '.' => Some(Token::Operator(Operator::Dot)),
+                    '(' => Some(Token::LeftParen),
+                    ')' => Some(Token::RightParen),
+                    ';' => Some(Token::Semicolon),
+                    _ => None,
+                }
+            }
+            fn is_delim(c: char) -> bool {
+                Self::from_delim(c).is_some()
+            }
+            fn from_id(id: &str) -> Self {
+                if id == "1" {
+                    Token::Unity
+                } else {
+                    Token::Id(id.into())
+                }
+            }
+        }
+
+        struct Tokenizer<'a>(&'a str);
+
+        impl<'a> Iterator for Tokenizer<'a> {
+            type Item = Token;
+
+            fn next(&mut self) -> Option<Self::Item> {
+                let mut iter = self.0.chars();
+                if let Some(delim) = Token::from_delim(iter.next()?) {
+                    self.0 = iter.as_str();
+                    return Some(delim);
+                }
+                loop {
+                    let (id, rest) = self.0.split_at(self.0.len() - iter.as_str().len());
+                    if iter.next().is_none_or(Token::is_delim) {
+                        self.0 = rest;
+                        return Some(Token::from_id(id));
+                    }
+                }
+            }
+        }
+
+        fn match_operator<F>(lexer: &mut Peekable<Tokenizer>, predicate: F) -> Option<Operator>
+        where
+            F: Fn(Operator) -> bool,
+        {
+            lexer.next_if_map(|token| {
+                if let Token::Operator(operator) = token
+                    && predicate(operator)
+                {
+                    Ok(operator)
+                } else {
+                    Err(token)
+                }
+            })
+        }
+
+        fn parse_operator<P, I>(
+            lexer: &mut Peekable<Tokenizer>,
+            predicate: P,
+            inner: I,
+        ) -> Result<Expression, ExpressionError>
+        where
+            P: Fn(Operator) -> bool,
+            I: Fn(&mut Peekable<Tokenizer>) -> Result<Expression, ExpressionError>,
+        {
+            let mut retval = inner(lexer)?;
+            while let Some(operator) = match_operator(lexer, &predicate) {
+                let rhs = inner(lexer)?;
+                retval = Expression::new_operation(operator, retval, rhs);
+            }
+            Ok(retval)
+        }
+
+        fn blend<'a>(lexer: &mut Peekable<Tokenizer<'a>>) -> Result<Expression, ExpressionError> {
+            parse_operator(lexer, |op| op == Operator::Blend, cross_dot)
+        }
+
+        fn cross_dot<'a>(
+            lexer: &mut Peekable<Tokenizer<'a>>,
+        ) -> Result<Expression, ExpressionError> {
+            parse_operator(
+                lexer,
+                |op| op == Operator::Cross || op == Operator::Dot,
+                nest,
+            )
+        }
+
+        fn nest<'a>(lexer: &mut Peekable<Tokenizer<'a>>) -> Result<Expression, ExpressionError> {
+            parse_operator(lexer, |op| op == Operator::Nest, primary)
+        }
+
+        fn primary<'a>(lexer: &mut Peekable<Tokenizer<'a>>) -> Result<Expression, ExpressionError> {
+            match lexer.next() {
+                Some(Token::Unity) => Ok(Expression::Unity),
+                Some(Token::Id(id)) => Ok(Expression::Variable(id)),
+                Some(Token::LeftParen) => {
+                    let retval = blend(lexer)?;
+                    if lexer.next_if_eq(&Token::RightParen).is_none() {
+                        return Err(ExpressionError::ExpectedRightParen);
+                    }
+                    Ok(retval)
+                }
+                Some(_) => Err(ExpressionError::SyntaxError),
+                None => Err(ExpressionError::UnexpectedEof),
+            }
+        }
+
+        let mut lexer = Tokenizer(s).peekable();
+        if lexer.peek().is_none() {
+            return Err(ExpressionError::EmptyString);
+        }
+        let expression = blend(&mut lexer)?;
+        if lexer.peek().is_some() {
+            return Err(ExpressionError::TrailingGarbage);
+        }
+        Ok(expression)
+    }
+}
+
+struct VarSet {
+    data: Vec<Series>,
+    //domain: Domain,
+}
+
+#[cfg(test)]
+mod tests {
+    use std::str::FromStr;
+
+    use crate::spv::read::graph::var_set::{Expression, ExpressionError, Operator};
+
+    #[test]
+    fn parse_expression() {
+        assert_eq!(Expression::from_str("1"), Ok(Expression::Unity));
+        assert_eq!(
+            Expression::from_str("abc"),
+            Ok(Expression::new_variable("abc"))
+        );
+        assert_eq!(
+            Expression::from_str("a+1"),
+            Ok(Expression::new_operation(
+                Operator::Blend,
+                Expression::new_variable("a"),
+                Expression::Unity
+            ))
+        );
+        assert_eq!(
+            Expression::from_str("abc"),
+            Ok(Expression::new_variable("abc"))
+        );
+        for (s, operator) in [
+            ("abc+def", Operator::Blend),
+            ("abc*def", Operator::Cross),
+            ("abc/def", Operator::Nest),
+            ("abc.def", Operator::Dot),
+        ] {
+            assert_eq!(
+                Expression::from_str(s),
+                Ok(Expression::new_operation(
+                    operator,
+                    Expression::new_variable("abc"),
+                    Expression::new_variable("def")
+                ))
+            );
+        }
+
+        #[derive(Copy, Clone, Debug, PartialEq, Eq)]
+        enum Side {
+            Left,
+            Right,
+        }
+        use Side::*;
+        for (s, operator1, operator2, higher) in [
+            // All the possible combinations of operators.
+            ("abc+def+ghi", Operator::Blend, Operator::Blend, Left),
+            ("abc*def+ghi", Operator::Cross, Operator::Blend, Left),
+            ("abc/def+ghi", Operator::Nest, Operator::Blend, Left),
+            ("abc.def+ghi", Operator::Dot, Operator::Blend, Left),
+            ("abc+def*ghi", Operator::Blend, Operator::Cross, Right),
+            ("abc*def*ghi", Operator::Cross, Operator::Cross, Left),
+            ("abc/def*ghi", Operator::Nest, Operator::Cross, Left),
+            ("abc.def*ghi", Operator::Dot, Operator::Cross, Left),
+            ("abc+def/ghi", Operator::Blend, Operator::Nest, Right),
+            ("abc*def/ghi", Operator::Cross, Operator::Nest, Right),
+            ("abc/def/ghi", Operator::Nest, Operator::Nest, Left),
+            ("abc.def/ghi", Operator::Dot, Operator::Nest, Right),
+            ("abc+def.ghi", Operator::Blend, Operator::Dot, Right),
+            ("abc*def.ghi", Operator::Cross, Operator::Dot, Left),
+            ("abc/def.ghi", Operator::Nest, Operator::Dot, Left),
+            ("abc.def.ghi", Operator::Dot, Operator::Dot, Left),
+            // Adding parentheses that match the default precedence.
+            ("(abc+def)+ghi", Operator::Blend, Operator::Blend, Left),
+            ("(abc*def)+ghi", Operator::Cross, Operator::Blend, Left),
+            ("(abc/def)+ghi", Operator::Nest, Operator::Blend, Left),
+            ("(abc.def)+ghi", Operator::Dot, Operator::Blend, Left),
+            ("abc+(def*ghi)", Operator::Blend, Operator::Cross, Right),
+            ("(abc*def)*ghi", Operator::Cross, Operator::Cross, Left),
+            ("(abc/def)*ghi", Operator::Nest, Operator::Cross, Left),
+            ("(abc.def)*ghi", Operator::Dot, Operator::Cross, Left),
+            ("abc+(def/ghi)", Operator::Blend, Operator::Nest, Right),
+            ("abc*(def/ghi)", Operator::Cross, Operator::Nest, Right),
+            ("(abc/def)/ghi", Operator::Nest, Operator::Nest, Left),
+            ("abc.(def/ghi)", Operator::Dot, Operator::Nest, Right),
+            ("abc+(def.ghi)", Operator::Blend, Operator::Dot, Right),
+            ("(abc*def).ghi", Operator::Cross, Operator::Dot, Left),
+            ("(abc/def).ghi", Operator::Nest, Operator::Dot, Left),
+            ("(abc.def).ghi", Operator::Dot, Operator::Dot, Left),
+            // Adding parentheses that reverse the default precedence.
+            ("abc+(def+ghi)", Operator::Blend, Operator::Blend, Right),
+            ("abc*(def+ghi)", Operator::Cross, Operator::Blend, Right),
+            ("abc/(def+ghi)", Operator::Nest, Operator::Blend, Right),
+            ("abc.(def+ghi)", Operator::Dot, Operator::Blend, Right),
+            ("(abc+def)*ghi", Operator::Blend, Operator::Cross, Left),
+            ("abc*(def*ghi)", Operator::Cross, Operator::Cross, Right),
+            ("abc/(def*ghi)", Operator::Nest, Operator::Cross, Right),
+            ("abc.(def*ghi)", Operator::Dot, Operator::Cross, Right),
+            ("(abc+def)/ghi", Operator::Blend, Operator::Nest, Left),
+            ("(abc*def)/ghi", Operator::Cross, Operator::Nest, Left),
+            ("abc/(def/ghi)", Operator::Nest, Operator::Nest, Right),
+            ("(abc.def)/ghi", Operator::Dot, Operator::Nest, Left),
+            ("(abc+def).ghi", Operator::Blend, Operator::Dot, Left),
+            ("abc*(def.ghi)", Operator::Cross, Operator::Dot, Right),
+            ("abc/(def.ghi)", Operator::Nest, Operator::Dot, Right),
+            ("abc.(def.ghi)", Operator::Dot, Operator::Dot, Right),
+        ] {
+            let operand1 = Expression::new_variable("abc");
+            let operand2 = Expression::new_variable("def");
+            let operand3 = Expression::new_variable("ghi");
+            let expected = match higher {
+                Left => Expression::new_operation(
+                    operator2,
+                    Expression::new_operation(operator1, operand1, operand2),
+                    operand3,
+                ),
+                Right => Expression::new_operation(
+                    operator1,
+                    operand1,
+                    Expression::new_operation(operator2, operand2, operand3),
+                ),
+            };
+            assert_eq!(
+                Expression::from_str(s),
+                Ok(expected),
+                "{s}, {operator1:?}, {operator2:?}, {higher:?}"
+            );
+        }
+
+        assert_eq!(Expression::from_str(""), Err(ExpressionError::EmptyString));
+        assert_eq!(
+            Expression::from_str("x)"),
+            Err(ExpressionError::TrailingGarbage)
+        );
+        assert_eq!(
+            Expression::from_str("(x"),
+            Err(ExpressionError::ExpectedRightParen)
+        );
+        assert_eq!(Expression::from_str("+"), Err(ExpressionError::SyntaxError));
+        assert_eq!(
+            Expression::from_str("x+"),
+            Err(ExpressionError::UnexpectedEof)
+        );
+    }
+}