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}