Skip to main content

clifford_codegen/symbolic/
parser.rs

1//! Constraint expression parsing.
2//!
3//! This module parses constraint strings like `s * s + xy * xy = 1` into
4//! Symbolica expressions for symbolic manipulation.
5
6use std::borrow::Cow;
7
8use symbolica::atom::{Atom, DefaultNamespace};
9use symbolica::parser::ParseSettings;
10use thiserror::Error;
11
12/// Errors that can occur during constraint parsing.
13#[derive(Debug, Error)]
14pub enum ParseError {
15    /// Missing equality operator in constraint.
16    #[error("constraint must contain '=' operator: {0}")]
17    MissingEquality(String),
18
19    /// Failed to parse expression.
20    #[error("failed to parse expression '{expr}': {reason}")]
21    InvalidExpression {
22        /// The expression that failed to parse.
23        expr: String,
24        /// The reason for the failure.
25        reason: String,
26    },
27}
28
29/// A parsed constraint expression of the form `lhs = rhs`.
30#[derive(Debug, Clone)]
31pub struct ConstraintExpr {
32    /// Left-hand side of the constraint.
33    pub lhs: Atom,
34    /// Right-hand side of the constraint.
35    pub rhs: Atom,
36    /// The original expression string.
37    pub original: String,
38}
39
40impl ConstraintExpr {
41    /// Returns the constraint as `lhs - rhs = 0` form.
42    pub fn as_zero_form(&self) -> Atom {
43        &self.lhs - &self.rhs
44    }
45}
46
47/// Parser for constraint expressions.
48///
49/// Converts constraint strings into Symbolica atoms for symbolic manipulation.
50#[derive(Debug, Default)]
51pub struct ConstraintParser;
52
53impl ConstraintParser {
54    /// Creates a new constraint parser.
55    pub fn new() -> Self {
56        Self
57    }
58
59    /// Parses a constraint expression.
60    ///
61    /// The expression must be of the form `expr = value`, where:
62    /// - `expr` is a mathematical expression using field names
63    /// - `value` is a numeric constant or expression
64    ///
65    /// # Arguments
66    ///
67    /// * `expr` - The constraint expression string (e.g., "s * s + xy * xy = 1")
68    /// * `field_names` - The field names that appear in the expression
69    ///
70    /// # Returns
71    ///
72    /// A parsed constraint expression.
73    ///
74    /// # Examples
75    ///
76    /// ```ignore
77    /// use clifford_codegen::symbolic::ConstraintParser;
78    ///
79    /// let parser = ConstraintParser::new();
80    /// let constraint = parser.parse("s * s + xy * xy = 1", &["s", "xy"])?;
81    /// ```
82    pub fn parse(&self, expr: &str, field_names: &[&str]) -> Result<ConstraintExpr, ParseError> {
83        // Split on '=' to get lhs and rhs
84        let parts: Vec<&str> = expr.split('=').collect();
85        if parts.len() != 2 {
86            return Err(ParseError::MissingEquality(expr.to_string()));
87        }
88
89        let lhs_str = parts[0].trim();
90        let rhs_str = parts[1].trim();
91
92        // Rename fields to valid Symbolica identifiers if needed
93        let lhs_normalized = self.normalize_fields(lhs_str, field_names);
94        let rhs_normalized = self.normalize_fields(rhs_str, field_names);
95
96        // Parse using Symbolica
97        let lhs = self.parse_expression(&lhs_normalized, expr)?;
98        let rhs = self.parse_expression(&rhs_normalized, expr)?;
99
100        Ok(ConstraintExpr {
101            lhs,
102            rhs,
103            original: expr.to_string(),
104        })
105    }
106
107    /// Normalizes field names in an expression.
108    ///
109    /// Some field names might conflict with Symbolica reserved words or
110    /// need special handling.
111    fn normalize_fields(&self, expr: &str, _field_names: &[&str]) -> String {
112        // For now, field names are used as-is since Symbolica handles
113        // arbitrary identifiers well
114        expr.to_string()
115    }
116
117    /// Parses a mathematical expression into a Symbolica Atom.
118    fn parse_expression(&self, expr: &str, original: &str) -> Result<Atom, ParseError> {
119        // Create a DefaultNamespace for the input using the crate's namespace
120        let input = DefaultNamespace {
121            namespace: Cow::Borrowed(env!("CARGO_CRATE_NAME")),
122            data: expr,
123            file: Cow::Borrowed(file!()),
124            line: line!() as usize,
125        };
126
127        // Parse using Symbolica
128        Atom::parse(input, ParseSettings::symbolica()).map_err(|e| ParseError::InvalidExpression {
129            expr: original.to_string(),
130            reason: e.to_string(),
131        })
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use std::sync::Mutex;
139
140    // Symbolica uses global state that conflicts when tests run in parallel.
141    // Tests prefixed with `symbolica_` are configured to run serially via nextest.
142    // The mutex provides a fallback for `cargo test` users.
143    static SYMBOLICA_LOCK: Mutex<()> = Mutex::new(());
144
145    #[test]
146    fn symbolica_parse_constraint_basic() {
147        let _guard = SYMBOLICA_LOCK.lock().unwrap();
148        let parser = ConstraintParser::new();
149        // Use numeric constraint to avoid symbol conflicts
150        let result = parser.parse("1 + 2 = 3", &[]);
151        assert!(result.is_ok());
152        let constraint = result.unwrap();
153        assert_eq!(constraint.original, "1 + 2 = 3");
154    }
155
156    #[test]
157    fn reject_missing_equality() {
158        // This test doesn't use Symbolica parsing at all - just string splitting
159        let parser = ConstraintParser::new();
160        let result = parser.parse("no equals sign", &[]);
161        assert!(matches!(result, Err(ParseError::MissingEquality(_))));
162    }
163}