Skip to main content

ipfrs_tensorlogic/
symbolic_expression_simplifier.rs

1//! Symbolic Expression Simplifier — multi-pass rewriting engine for symbolic math expressions.
2//!
3//! Provides:
4//! - A rich expression type (`SesExpr`) covering arithmetic, power, trig, exp/ln
5//! - Multi-pass fixpoint simplification with configurable rule sets
6//! - Symbolic differentiation and substitution
7//! - A recursive-descent parser for infix expressions
8//! - Full numeric evaluation with variable bindings
9//! - Simplification history tracking (bounded VecDeque)
10
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::fmt;
13
14// ---------------------------------------------------------------------------
15// Error type
16// ---------------------------------------------------------------------------
17
18/// Errors that can occur during symbolic expression operations.
19#[derive(Debug, Clone, PartialEq)]
20pub enum SesError {
21    /// A variable was not found in the provided bindings.
22    UnboundVariable(String),
23    /// A numeric operation produced an invalid result (e.g. ln of negative).
24    MathDomainError(String),
25    /// A parse error with a descriptive message.
26    ParseError(String),
27    /// Division by zero attempted during evaluation.
28    DivisionByZero,
29}
30
31impl fmt::Display for SesError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            SesError::UnboundVariable(v) => write!(f, "unbound variable: {v}"),
35            SesError::MathDomainError(m) => write!(f, "math domain error: {m}"),
36            SesError::ParseError(m) => write!(f, "parse error: {m}"),
37            SesError::DivisionByZero => write!(f, "division by zero"),
38        }
39    }
40}
41
42impl std::error::Error for SesError {}
43
44// ---------------------------------------------------------------------------
45// Core expression type
46// ---------------------------------------------------------------------------
47
48/// A symbolic mathematical expression.
49#[derive(Debug, Clone, PartialEq)]
50pub enum SesExpr {
51    /// A numeric literal.
52    Num(f64),
53    /// A named variable.
54    Var(String),
55    /// Addition: left + right.
56    Add(Box<SesExpr>, Box<SesExpr>),
57    /// Subtraction: left - right.
58    Sub(Box<SesExpr>, Box<SesExpr>),
59    /// Multiplication: left * right.
60    Mul(Box<SesExpr>, Box<SesExpr>),
61    /// Division: left / right.
62    Div(Box<SesExpr>, Box<SesExpr>),
63    /// Exponentiation: base ^ exponent.
64    Pow(Box<SesExpr>, Box<SesExpr>),
65    /// Negation: -expr.
66    Neg(Box<SesExpr>),
67    /// Absolute value: |expr|.
68    Abs(Box<SesExpr>),
69    /// Square root: √expr.
70    Sqrt(Box<SesExpr>),
71    /// Sine: sin(expr).
72    Sin(Box<SesExpr>),
73    /// Cosine: cos(expr).
74    Cos(Box<SesExpr>),
75    /// Natural exponential: e^expr.
76    Exp(Box<SesExpr>),
77    /// Natural logarithm: ln(expr).
78    Ln(Box<SesExpr>),
79}
80
81impl SesExpr {
82    /// Convenience constructor for a boxed `Num`.
83    #[inline]
84    pub fn num(v: f64) -> Self {
85        SesExpr::Num(v)
86    }
87    /// Convenience constructor for `Var`.
88    #[inline]
89    pub fn var(name: impl Into<String>) -> Self {
90        SesExpr::Var(name.into())
91    }
92    /// Convenience constructor for `Add`.
93    #[inline]
94    #[allow(clippy::should_implement_trait)]
95    pub fn add(l: SesExpr, r: SesExpr) -> Self {
96        SesExpr::Add(Box::new(l), Box::new(r))
97    }
98    /// Convenience constructor for `Sub`.
99    #[inline]
100    #[allow(clippy::should_implement_trait)]
101    pub fn sub(l: SesExpr, r: SesExpr) -> Self {
102        SesExpr::Sub(Box::new(l), Box::new(r))
103    }
104    /// Convenience constructor for `Mul`.
105    #[inline]
106    #[allow(clippy::should_implement_trait)]
107    pub fn mul(l: SesExpr, r: SesExpr) -> Self {
108        SesExpr::Mul(Box::new(l), Box::new(r))
109    }
110    /// Convenience constructor for `Div`.
111    #[inline]
112    #[allow(clippy::should_implement_trait)]
113    pub fn div(l: SesExpr, r: SesExpr) -> Self {
114        SesExpr::Div(Box::new(l), Box::new(r))
115    }
116    /// Convenience constructor for `Pow`.
117    #[inline]
118    pub fn pow(b: SesExpr, e: SesExpr) -> Self {
119        SesExpr::Pow(Box::new(b), Box::new(e))
120    }
121    /// Convenience constructor for `Neg`.
122    #[inline]
123    #[allow(clippy::should_implement_trait)]
124    pub fn neg(e: SesExpr) -> Self {
125        SesExpr::Neg(Box::new(e))
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Display / infix notation
131// ---------------------------------------------------------------------------
132
133impl fmt::Display for SesExpr {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "{}", infix_str(self, Precedence::Lowest))
136    }
137}
138
139/// Internal precedence levels for minimal-parentheses infix printing.
140#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
141enum Precedence {
142    Lowest,
143    AddSub,
144    MulDiv,
145    Unary,
146    Pow,
147}
148
149fn infix_str(e: &SesExpr, parent_prec: Precedence) -> String {
150    match e {
151        SesExpr::Num(v) => {
152            if v.fract() == 0.0 && v.abs() < 1e15 {
153                format!("{}", *v as i64)
154            } else {
155                format!("{v}")
156            }
157        }
158        SesExpr::Var(name) => name.clone(),
159        SesExpr::Add(l, r) => paren_if(
160            &format!(
161                "{} + {}",
162                infix_str(l, Precedence::AddSub),
163                infix_str(r, Precedence::AddSub)
164            ),
165            Precedence::AddSub,
166            parent_prec,
167        ),
168        SesExpr::Sub(l, r) => paren_if(
169            &format!(
170                "{} - {}",
171                infix_str(l, Precedence::AddSub),
172                infix_str(r, Precedence::MulDiv)
173            ),
174            Precedence::AddSub,
175            parent_prec,
176        ),
177        SesExpr::Mul(l, r) => paren_if(
178            &format!(
179                "{} * {}",
180                infix_str(l, Precedence::MulDiv),
181                infix_str(r, Precedence::MulDiv)
182            ),
183            Precedence::MulDiv,
184            parent_prec,
185        ),
186        SesExpr::Div(l, r) => paren_if(
187            &format!(
188                "{} / {}",
189                infix_str(l, Precedence::MulDiv),
190                infix_str(r, Precedence::Unary)
191            ),
192            Precedence::MulDiv,
193            parent_prec,
194        ),
195        SesExpr::Pow(b, exp) => paren_if(
196            &format!(
197                "{} ^ {}",
198                infix_str(b, Precedence::Unary),
199                infix_str(exp, Precedence::Pow)
200            ),
201            Precedence::Pow,
202            parent_prec,
203        ),
204        SesExpr::Neg(inner) => paren_if(
205            &format!("-{}", infix_str(inner, Precedence::Unary)),
206            Precedence::Unary,
207            parent_prec,
208        ),
209        SesExpr::Abs(inner) => format!("|{}|", infix_str(inner, Precedence::Lowest)),
210        SesExpr::Sqrt(inner) => format!("sqrt({})", infix_str(inner, Precedence::Lowest)),
211        SesExpr::Sin(inner) => format!("sin({})", infix_str(inner, Precedence::Lowest)),
212        SesExpr::Cos(inner) => format!("cos({})", infix_str(inner, Precedence::Lowest)),
213        SesExpr::Exp(inner) => format!("exp({})", infix_str(inner, Precedence::Lowest)),
214        SesExpr::Ln(inner) => format!("ln({})", infix_str(inner, Precedence::Lowest)),
215    }
216}
217
218fn paren_if(s: &str, my_prec: Precedence, parent_prec: Precedence) -> String {
219    if my_prec < parent_prec {
220        format!("({s})")
221    } else {
222        s.to_string()
223    }
224}
225
226// ---------------------------------------------------------------------------
227// Simplifier configuration
228// ---------------------------------------------------------------------------
229
230/// Configuration for the `SymbolicExpressionSimplifier`.
231#[derive(Debug, Clone)]
232pub struct SesSimplifierConfig {
233    /// Maximum number of simplification passes before giving up.
234    pub max_passes: usize,
235    /// Enable constant folding (evaluate sub-expressions with no variables).
236    pub enable_constant_folding: bool,
237    /// Enable algebraic identity rules (e.g. x+0, x*1).
238    pub enable_algebraic_rules: bool,
239    /// Enable trigonometric identities (e.g. sin²+cos²=1).
240    pub enable_trig_rules: bool,
241}
242
243impl Default for SesSimplifierConfig {
244    fn default() -> Self {
245        SesSimplifierConfig {
246            max_passes: 32,
247            enable_constant_folding: true,
248            enable_algebraic_rules: true,
249            enable_trig_rules: true,
250        }
251    }
252}
253
254// ---------------------------------------------------------------------------
255// Rewrite rule
256// ---------------------------------------------------------------------------
257
258/// A named rewrite rule for the simplifier.
259pub struct SesRewriteRule {
260    /// Human-readable name for the rule.
261    pub name: String,
262    /// A short pattern description string (informational only).
263    pub pattern: String,
264    /// Predicate: returns `true` when this rule can be applied to `expr`.
265    pub applies: fn(&SesExpr) -> bool,
266    /// Transformation: consumes the expression and returns the simplified form.
267    pub transform: Box<dyn Fn(SesExpr) -> SesExpr + Send + Sync>,
268}
269
270impl fmt::Debug for SesRewriteRule {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        f.debug_struct("SesRewriteRule")
273            .field("name", &self.name)
274            .field("pattern", &self.pattern)
275            .finish()
276    }
277}
278
279// ---------------------------------------------------------------------------
280// Simplification step (history entry)
281// ---------------------------------------------------------------------------
282
283/// A single recorded simplification step.
284#[derive(Debug, Clone)]
285pub struct SesSimplificationStep {
286    /// Which pass (0-indexed) this step occurred on.
287    pub pass: usize,
288    /// Name of the rule that was applied.
289    pub rule_applied: String,
290    /// Infix string of the expression *before* the rewrite.
291    pub before: String,
292    /// Infix string of the expression *after* the rewrite.
293    pub after: String,
294}
295
296// ---------------------------------------------------------------------------
297// Statistics
298// ---------------------------------------------------------------------------
299
300/// Summary statistics for the simplifier.
301#[derive(Debug, Clone, Default)]
302pub struct SesSimplifierStats {
303    /// Total number of `simplify` calls made.
304    pub simplify_calls: usize,
305    /// Total simplification passes executed across all calls.
306    pub total_passes: usize,
307    /// Total number of individual rule applications.
308    pub total_rule_applications: usize,
309    /// Number of simplifications that reached fixpoint before max_passes.
310    pub fixpoint_reached: usize,
311    /// Total `evaluate` calls.
312    pub evaluate_calls: usize,
313    /// Total `differentiate` calls.
314    pub differentiate_calls: usize,
315    /// History entries currently stored.
316    pub history_len: usize,
317}
318
319// ---------------------------------------------------------------------------
320// Type aliases (as required by the task)
321// ---------------------------------------------------------------------------
322
323/// Alias for `SymbolicExpressionSimplifier`.
324pub type SesSymbolicExpressionSimplifier = SymbolicExpressionSimplifier;
325
326// ---------------------------------------------------------------------------
327// Main struct
328// ---------------------------------------------------------------------------
329
330/// A multi-pass symbolic expression simplifier with rewriting rules and
331/// canonical forms.
332pub struct SymbolicExpressionSimplifier {
333    /// Ordered set of rewrite rules.
334    rules: Vec<SesRewriteRule>,
335    /// Bounded history of simplification steps (max 500 entries).
336    history: VecDeque<SesSimplificationStep>,
337    /// Configuration.
338    config: SesSimplifierConfig,
339    /// Accumulated statistics.
340    stats: SesSimplifierStats,
341}
342
343impl SymbolicExpressionSimplifier {
344    // -----------------------------------------------------------------------
345    // Construction
346    // -----------------------------------------------------------------------
347
348    /// Create a new simplifier with the default configuration and built-in rules.
349    pub fn new() -> Self {
350        let config = SesSimplifierConfig::default();
351        Self::with_config(config)
352    }
353
354    /// Create a new simplifier with a custom configuration.
355    pub fn with_config(config: SesSimplifierConfig) -> Self {
356        let mut s = SymbolicExpressionSimplifier {
357            rules: Vec::new(),
358            history: VecDeque::with_capacity(500),
359            config,
360            stats: SesSimplifierStats::default(),
361        };
362        s.register_builtin_rules();
363        s
364    }
365
366    /// Add a custom rewrite rule.
367    pub fn add_rule(&mut self, rule: SesRewriteRule) {
368        self.rules.push(rule);
369    }
370
371    // -----------------------------------------------------------------------
372    // Built-in rules
373    // -----------------------------------------------------------------------
374
375    fn register_builtin_rules(&mut self) {
376        // x + 0 → x
377        self.rules.push(SesRewriteRule {
378            name: "add_zero_right".into(),
379            pattern: "x + 0 => x".into(),
380            applies: |e| matches!(e, SesExpr::Add(_, r) if is_zero(r)),
381            transform: Box::new(|e| {
382                if let SesExpr::Add(l, r) = e {
383                    if is_zero(&r) {
384                        return *l;
385                    }
386                    SesExpr::Add(l, r)
387                } else {
388                    e
389                }
390            }),
391        });
392        // 0 + x → x
393        self.rules.push(SesRewriteRule {
394            name: "add_zero_left".into(),
395            pattern: "0 + x => x".into(),
396            applies: |e| matches!(e, SesExpr::Add(l, _) if is_zero(l)),
397            transform: Box::new(|e| {
398                if let SesExpr::Add(l, r) = e {
399                    if is_zero(&l) {
400                        return *r;
401                    }
402                    SesExpr::Add(l, r)
403                } else {
404                    e
405                }
406            }),
407        });
408        // x - 0 → x
409        self.rules.push(SesRewriteRule {
410            name: "sub_zero".into(),
411            pattern: "x - 0 => x".into(),
412            applies: |e| matches!(e, SesExpr::Sub(_, r) if is_zero(r)),
413            transform: Box::new(|e| {
414                if let SesExpr::Sub(l, r) = e {
415                    if is_zero(&r) {
416                        return *l;
417                    }
418                    SesExpr::Sub(l, r)
419                } else {
420                    e
421                }
422            }),
423        });
424        // 0 - x → -x
425        self.rules.push(SesRewriteRule {
426            name: "zero_sub".into(),
427            pattern: "0 - x => -x".into(),
428            applies: |e| matches!(e, SesExpr::Sub(l, _) if is_zero(l)),
429            transform: Box::new(|e| {
430                if let SesExpr::Sub(l, r) = e {
431                    if is_zero(&l) {
432                        return SesExpr::Neg(r);
433                    }
434                    SesExpr::Sub(l, r)
435                } else {
436                    e
437                }
438            }),
439        });
440        // x * 1 → x
441        self.rules.push(SesRewriteRule {
442            name: "mul_one_right".into(),
443            pattern: "x * 1 => x".into(),
444            applies: |e| matches!(e, SesExpr::Mul(_, r) if is_one(r)),
445            transform: Box::new(|e| {
446                if let SesExpr::Mul(l, r) = e {
447                    if is_one(&r) {
448                        return *l;
449                    }
450                    SesExpr::Mul(l, r)
451                } else {
452                    e
453                }
454            }),
455        });
456        // 1 * x → x
457        self.rules.push(SesRewriteRule {
458            name: "mul_one_left".into(),
459            pattern: "1 * x => x".into(),
460            applies: |e| matches!(e, SesExpr::Mul(l, _) if is_one(l)),
461            transform: Box::new(|e| {
462                if let SesExpr::Mul(l, r) = e {
463                    if is_one(&l) {
464                        return *r;
465                    }
466                    SesExpr::Mul(l, r)
467                } else {
468                    e
469                }
470            }),
471        });
472        // x * 0 → 0
473        self.rules.push(SesRewriteRule {
474            name: "mul_zero_right".into(),
475            pattern: "x * 0 => 0".into(),
476            applies: |e| matches!(e, SesExpr::Mul(_, r) if is_zero(r)),
477            transform: Box::new(|e| {
478                if let SesExpr::Mul(_, r) = e {
479                    if is_zero(&r) {
480                        return SesExpr::Num(0.0);
481                    }
482                    SesExpr::Mul(Box::new(SesExpr::Num(0.0)), r)
483                } else {
484                    e
485                }
486            }),
487        });
488        // 0 * x → 0
489        self.rules.push(SesRewriteRule {
490            name: "mul_zero_left".into(),
491            pattern: "0 * x => 0".into(),
492            applies: |e| matches!(e, SesExpr::Mul(l, _) if is_zero(l)),
493            transform: Box::new(|e| {
494                if let SesExpr::Mul(l, _) = e {
495                    if is_zero(&l) {
496                        return SesExpr::Num(0.0);
497                    }
498                    SesExpr::Mul(l, Box::new(SesExpr::Num(0.0)))
499                } else {
500                    e
501                }
502            }),
503        });
504        // x / 1 → x
505        self.rules.push(SesRewriteRule {
506            name: "div_one".into(),
507            pattern: "x / 1 => x".into(),
508            applies: |e| matches!(e, SesExpr::Div(_, r) if is_one(r)),
509            transform: Box::new(|e| {
510                if let SesExpr::Div(l, r) = e {
511                    if is_one(&r) {
512                        return *l;
513                    }
514                    SesExpr::Div(l, r)
515                } else {
516                    e
517                }
518            }),
519        });
520        // 0 / x → 0  (x ≠ 0 structurally — we do it when denominator is non-zero literal)
521        self.rules.push(SesRewriteRule {
522            name: "zero_div".into(),
523            pattern: "0 / x => 0 (x != 0)".into(),
524            applies: |e| matches!(e, SesExpr::Div(l, r) if is_zero(l) && !is_zero(r)),
525            transform: Box::new(|e| {
526                if let SesExpr::Div(l, r) = e {
527                    if is_zero(&l) && !is_zero(&r) {
528                        return SesExpr::Num(0.0);
529                    }
530                    SesExpr::Div(l, r)
531                } else {
532                    e
533                }
534            }),
535        });
536        // x ^ 1 → x
537        self.rules.push(SesRewriteRule {
538            name: "pow_one".into(),
539            pattern: "x ^ 1 => x".into(),
540            applies: |e| matches!(e, SesExpr::Pow(_, r) if is_one(r)),
541            transform: Box::new(|e| {
542                if let SesExpr::Pow(b, r) = e {
543                    if is_one(&r) {
544                        return *b;
545                    }
546                    SesExpr::Pow(b, r)
547                } else {
548                    e
549                }
550            }),
551        });
552        // x ^ 0 → 1
553        self.rules.push(SesRewriteRule {
554            name: "pow_zero".into(),
555            pattern: "x ^ 0 => 1".into(),
556            applies: |e| matches!(e, SesExpr::Pow(_, r) if is_zero(r)),
557            transform: Box::new(|e| {
558                if let SesExpr::Pow(_, r) = e {
559                    if is_zero(&r) {
560                        return SesExpr::Num(1.0);
561                    }
562                    SesExpr::Pow(Box::new(SesExpr::Num(0.0)), r)
563                } else {
564                    e
565                }
566            }),
567        });
568        // 1 ^ x → 1
569        self.rules.push(SesRewriteRule {
570            name: "one_pow".into(),
571            pattern: "1 ^ x => 1".into(),
572            applies: |e| matches!(e, SesExpr::Pow(b, _) if is_one(b)),
573            transform: Box::new(|e| {
574                if let SesExpr::Pow(b, _) = e {
575                    if is_one(&b) {
576                        return SesExpr::Num(1.0);
577                    }
578                    SesExpr::Pow(b, Box::new(SesExpr::Num(0.0)))
579                } else {
580                    e
581                }
582            }),
583        });
584        // --x → x  (double negation)
585        self.rules.push(SesRewriteRule {
586            name: "double_neg".into(),
587            pattern: "--x => x".into(),
588            applies: |e| matches!(e, SesExpr::Neg(inner) if matches!(**inner, SesExpr::Neg(_))),
589            transform: Box::new(|e| {
590                if let SesExpr::Neg(inner) = e {
591                    if let SesExpr::Neg(inner2) = *inner {
592                        return *inner2;
593                    }
594                    SesExpr::Neg(inner)
595                } else {
596                    e
597                }
598            }),
599        });
600        // x - x → 0
601        self.rules.push(SesRewriteRule {
602            name: "sub_self".into(),
603            pattern: "x - x => 0".into(),
604            applies: |e| matches!(e, SesExpr::Sub(l, r) if exprs_equal(l, r)),
605            transform: Box::new(|e| {
606                if let SesExpr::Sub(l, r) = e {
607                    if exprs_equal(&l, &r) {
608                        return SesExpr::Num(0.0);
609                    }
610                    SesExpr::Sub(l, r)
611                } else {
612                    e
613                }
614            }),
615        });
616        // x / x → 1  (when x is non-trivially non-zero in structure)
617        self.rules.push(SesRewriteRule {
618            name: "div_self".into(),
619            pattern: "x / x => 1".into(),
620            applies: |e| matches!(e, SesExpr::Div(l, r) if exprs_equal(l, r) && !is_zero(l)),
621            transform: Box::new(|e| {
622                if let SesExpr::Div(l, r) = e {
623                    if exprs_equal(&l, &r) && !is_zero(&l) {
624                        return SesExpr::Num(1.0);
625                    }
626                    SesExpr::Div(l, r)
627                } else {
628                    e
629                }
630            }),
631        });
632        // sqrt(x^2) → |x|
633        self.rules.push(SesRewriteRule {
634            name: "sqrt_sq".into(),
635            pattern: "sqrt(x^2) => |x|".into(),
636            applies: |e| matches!(e, SesExpr::Sqrt(inner) if matches!(inner.as_ref(), SesExpr::Pow(_, exp) if is_two(exp))),
637            transform: Box::new(|e| {
638                if let SesExpr::Sqrt(inner) = e {
639                    if let SesExpr::Pow(base, exp) = *inner {
640                        if is_two(&exp) { return SesExpr::Abs(base); }
641                        return SesExpr::Sqrt(Box::new(SesExpr::Pow(base, exp)));
642                    }
643                    SesExpr::Sqrt(inner)
644                } else { e }
645            }),
646        });
647        // exp(ln(x)) → x
648        self.rules.push(SesRewriteRule {
649            name: "exp_ln".into(),
650            pattern: "exp(ln(x)) => x".into(),
651            applies: |e| matches!(e, SesExpr::Exp(inner) if matches!(**inner, SesExpr::Ln(_))),
652            transform: Box::new(|e| {
653                if let SesExpr::Exp(inner) = e {
654                    if let SesExpr::Ln(x) = *inner {
655                        return *x;
656                    }
657                    SesExpr::Exp(inner)
658                } else {
659                    e
660                }
661            }),
662        });
663        // ln(exp(x)) → x
664        self.rules.push(SesRewriteRule {
665            name: "ln_exp".into(),
666            pattern: "ln(exp(x)) => x".into(),
667            applies: |e| matches!(e, SesExpr::Ln(inner) if matches!(**inner, SesExpr::Exp(_))),
668            transform: Box::new(|e| {
669                if let SesExpr::Ln(inner) = e {
670                    if let SesExpr::Exp(x) = *inner {
671                        return *x;
672                    }
673                    SesExpr::Ln(inner)
674                } else {
675                    e
676                }
677            }),
678        });
679        // ln(1) → 0
680        self.rules.push(SesRewriteRule {
681            name: "ln_one".into(),
682            pattern: "ln(1) => 0".into(),
683            applies: |e| matches!(e, SesExpr::Ln(inner) if is_one(inner)),
684            transform: Box::new(|e| {
685                if let SesExpr::Ln(inner) = e {
686                    if is_one(&inner) {
687                        return SesExpr::Num(0.0);
688                    }
689                    SesExpr::Ln(inner)
690                } else {
691                    e
692                }
693            }),
694        });
695        // exp(0) → 1
696        self.rules.push(SesRewriteRule {
697            name: "exp_zero".into(),
698            pattern: "exp(0) => 1".into(),
699            applies: |e| matches!(e, SesExpr::Exp(inner) if is_zero(inner)),
700            transform: Box::new(|e| {
701                if let SesExpr::Exp(inner) = e {
702                    if is_zero(&inner) {
703                        return SesExpr::Num(1.0);
704                    }
705                    SesExpr::Exp(inner)
706                } else {
707                    e
708                }
709            }),
710        });
711        // sqrt(0) → 0
712        self.rules.push(SesRewriteRule {
713            name: "sqrt_zero".into(),
714            pattern: "sqrt(0) => 0".into(),
715            applies: |e| matches!(e, SesExpr::Sqrt(inner) if is_zero(inner)),
716            transform: Box::new(|e| {
717                if let SesExpr::Sqrt(inner) = e {
718                    if is_zero(&inner) {
719                        return SesExpr::Num(0.0);
720                    }
721                    SesExpr::Sqrt(inner)
722                } else {
723                    e
724                }
725            }),
726        });
727        // sqrt(1) → 1
728        self.rules.push(SesRewriteRule {
729            name: "sqrt_one".into(),
730            pattern: "sqrt(1) => 1".into(),
731            applies: |e| matches!(e, SesExpr::Sqrt(inner) if is_one(inner)),
732            transform: Box::new(|e| {
733                if let SesExpr::Sqrt(inner) = e {
734                    if is_one(&inner) {
735                        return SesExpr::Num(1.0);
736                    }
737                    SesExpr::Sqrt(inner)
738                } else {
739                    e
740                }
741            }),
742        });
743        // -(-x) handled by double_neg; also: neg(0) → 0
744        self.rules.push(SesRewriteRule {
745            name: "neg_zero".into(),
746            pattern: "-0 => 0".into(),
747            applies: |e| matches!(e, SesExpr::Neg(inner) if is_zero(inner)),
748            transform: Box::new(|e| {
749                if let SesExpr::Neg(inner) = e {
750                    if is_zero(&inner) {
751                        return SesExpr::Num(0.0);
752                    }
753                    SesExpr::Neg(inner)
754                } else {
755                    e
756                }
757            }),
758        });
759        // sin²(x) + cos²(x) → 1  (trig identity)
760        self.rules.push(SesRewriteRule {
761            name: "sin2_cos2".into(),
762            pattern: "sin(x)^2 + cos(x)^2 => 1".into(),
763            applies: |e| is_sin2_plus_cos2(e),
764            transform: Box::new(|e| {
765                if is_sin2_plus_cos2(&e) {
766                    SesExpr::Num(1.0)
767                } else {
768                    e
769                }
770            }),
771        });
772        // x * x → x^2
773        self.rules.push(SesRewriteRule {
774            name: "mul_self_to_pow2".into(),
775            pattern: "x * x => x^2".into(),
776            applies: |e| matches!(e, SesExpr::Mul(l, r) if exprs_equal(l, r) && !matches!(**l, SesExpr::Num(_))),
777            transform: Box::new(|e| {
778                if let SesExpr::Mul(l, r) = e {
779                    if exprs_equal(&l, &r) && !matches!(*l, SesExpr::Num(_)) {
780                        return SesExpr::Pow(l, Box::new(SesExpr::Num(2.0)));
781                    }
782                    SesExpr::Mul(l, r)
783                } else { e }
784            }),
785        });
786        // x + x → 2*x
787        self.rules.push(SesRewriteRule {
788            name: "add_self".into(),
789            pattern: "x + x => 2*x".into(),
790            applies: |e| matches!(e, SesExpr::Add(l, r) if exprs_equal(l, r) && !matches!(**l, SesExpr::Num(_))),
791            transform: Box::new(|e| {
792                if let SesExpr::Add(l, r) = e {
793                    if exprs_equal(&l, &r) && !matches!(*l, SesExpr::Num(_)) {
794                        return SesExpr::Mul(Box::new(SesExpr::Num(2.0)), l);
795                    }
796                    SesExpr::Add(l, r)
797                } else { e }
798            }),
799        });
800    }
801
802    // -----------------------------------------------------------------------
803    // Core operations
804    // -----------------------------------------------------------------------
805
806    /// Simplify `expr` using multi-pass rewriting until fixpoint or `max_passes`.
807    pub fn simplify(&mut self, expr: &SesExpr) -> SesExpr {
808        self.stats.simplify_calls += 1;
809        let mut current = expr.clone();
810        let mut pass = 0;
811        let max = self.config.max_passes;
812        loop {
813            let before_pass = current.clone();
814            current = self.one_pass(current, pass);
815            pass += 1;
816            self.stats.total_passes += 1;
817            if exprs_equal(&current, &before_pass) {
818                self.stats.fixpoint_reached += 1;
819                break;
820            }
821            if pass >= max {
822                break;
823            }
824        }
825        current
826    }
827
828    /// Perform one full bottom-up rewriting pass.
829    fn one_pass(&mut self, expr: SesExpr, pass: usize) -> SesExpr {
830        // First recurse into children
831        let expr = self.recurse_children(expr, pass);
832        // Then try to apply constant folding
833        let expr = if self.config.enable_constant_folding {
834            self.fold_constants(expr, pass)
835        } else {
836            expr
837        };
838        // Then try each rule in order
839        self.apply_rules(expr, pass)
840    }
841
842    /// Recursively simplify all children of an expression.
843    fn recurse_children(&mut self, expr: SesExpr, pass: usize) -> SesExpr {
844        match expr {
845            SesExpr::Add(l, r) => SesExpr::Add(
846                Box::new(self.one_pass(*l, pass)),
847                Box::new(self.one_pass(*r, pass)),
848            ),
849            SesExpr::Sub(l, r) => SesExpr::Sub(
850                Box::new(self.one_pass(*l, pass)),
851                Box::new(self.one_pass(*r, pass)),
852            ),
853            SesExpr::Mul(l, r) => SesExpr::Mul(
854                Box::new(self.one_pass(*l, pass)),
855                Box::new(self.one_pass(*r, pass)),
856            ),
857            SesExpr::Div(l, r) => SesExpr::Div(
858                Box::new(self.one_pass(*l, pass)),
859                Box::new(self.one_pass(*r, pass)),
860            ),
861            SesExpr::Pow(b, e) => SesExpr::Pow(
862                Box::new(self.one_pass(*b, pass)),
863                Box::new(self.one_pass(*e, pass)),
864            ),
865            SesExpr::Neg(inner) => SesExpr::Neg(Box::new(self.one_pass(*inner, pass))),
866            SesExpr::Abs(inner) => SesExpr::Abs(Box::new(self.one_pass(*inner, pass))),
867            SesExpr::Sqrt(inner) => SesExpr::Sqrt(Box::new(self.one_pass(*inner, pass))),
868            SesExpr::Sin(inner) => SesExpr::Sin(Box::new(self.one_pass(*inner, pass))),
869            SesExpr::Cos(inner) => SesExpr::Cos(Box::new(self.one_pass(*inner, pass))),
870            SesExpr::Exp(inner) => SesExpr::Exp(Box::new(self.one_pass(*inner, pass))),
871            SesExpr::Ln(inner) => SesExpr::Ln(Box::new(self.one_pass(*inner, pass))),
872            atom => atom,
873        }
874    }
875
876    /// Try to fold constant sub-expressions.
877    fn fold_constants(&mut self, expr: SesExpr, pass: usize) -> SesExpr {
878        if !is_constant(&expr) {
879            return expr;
880        }
881        // Evaluate with an empty binding map (no variables)
882        let vars: HashMap<String, f64> = HashMap::new();
883        if let Ok(v) = self.evaluate_inner(&expr, &vars) {
884            if v.is_finite() {
885                let before = expr.to_string();
886                let folded = SesExpr::Num(v);
887                self.record_step(pass, "constant_folding", &before, &folded.to_string());
888                return folded;
889            }
890        }
891        expr
892    }
893
894    /// Apply the first matching rule to `expr`.
895    fn apply_rules(&mut self, expr: SesExpr, pass: usize) -> SesExpr {
896        // Collect indices of applicable rules to avoid borrow issues
897        let applicable: Vec<usize> = self
898            .rules
899            .iter()
900            .enumerate()
901            .filter(|(_, r)| {
902                let skip = (r.name.starts_with("sin2_cos2") && !self.config.enable_trig_rules)
903                    || (!self.config.enable_algebraic_rules && !r.name.starts_with("constant"));
904                if skip {
905                    return false;
906                }
907                (r.applies)(&expr)
908            })
909            .map(|(i, _)| i)
910            .collect();
911
912        if let Some(&idx) = applicable.first() {
913            let rule_name = self.rules[idx].name.clone();
914            let before = expr.to_string();
915            let result = (self.rules[idx].transform)(expr);
916            let after = result.to_string();
917            if before != after {
918                self.stats.total_rule_applications += 1;
919                self.record_step(pass, &rule_name, &before, &after);
920            }
921            result
922        } else {
923            expr
924        }
925    }
926
927    fn record_step(&mut self, pass: usize, rule: &str, before: &str, after: &str) {
928        if self.history.len() >= 500 {
929            self.history.pop_front();
930        }
931        self.history.push_back(SesSimplificationStep {
932            pass,
933            rule_applied: rule.to_string(),
934            before: before.to_string(),
935            after: after.to_string(),
936        });
937        self.stats.history_len = self.history.len();
938    }
939
940    // -----------------------------------------------------------------------
941    // Numeric evaluation
942    // -----------------------------------------------------------------------
943
944    /// Evaluate `expr` numerically given variable bindings.
945    pub fn evaluate(
946        &mut self,
947        expr: &SesExpr,
948        vars: &HashMap<String, f64>,
949    ) -> Result<f64, SesError> {
950        self.stats.evaluate_calls += 1;
951        self.evaluate_inner(expr, vars)
952    }
953
954    fn evaluate_inner(&self, expr: &SesExpr, vars: &HashMap<String, f64>) -> Result<f64, SesError> {
955        match expr {
956            SesExpr::Num(v) => Ok(*v),
957            SesExpr::Var(name) => vars
958                .get(name.as_str())
959                .copied()
960                .ok_or_else(|| SesError::UnboundVariable(name.clone())),
961            SesExpr::Add(l, r) => Ok(self.evaluate_inner(l, vars)? + self.evaluate_inner(r, vars)?),
962            SesExpr::Sub(l, r) => Ok(self.evaluate_inner(l, vars)? - self.evaluate_inner(r, vars)?),
963            SesExpr::Mul(l, r) => Ok(self.evaluate_inner(l, vars)? * self.evaluate_inner(r, vars)?),
964            SesExpr::Div(l, r) => {
965                let denom = self.evaluate_inner(r, vars)?;
966                if denom == 0.0 {
967                    return Err(SesError::DivisionByZero);
968                }
969                Ok(self.evaluate_inner(l, vars)? / denom)
970            }
971            SesExpr::Pow(b, e) => {
972                let base = self.evaluate_inner(b, vars)?;
973                let exp = self.evaluate_inner(e, vars)?;
974                Ok(base.powf(exp))
975            }
976            SesExpr::Neg(inner) => Ok(-self.evaluate_inner(inner, vars)?),
977            SesExpr::Abs(inner) => Ok(self.evaluate_inner(inner, vars)?.abs()),
978            SesExpr::Sqrt(inner) => {
979                let v = self.evaluate_inner(inner, vars)?;
980                if v < 0.0 {
981                    return Err(SesError::MathDomainError("sqrt of negative number".into()));
982                }
983                Ok(v.sqrt())
984            }
985            SesExpr::Sin(inner) => Ok(self.evaluate_inner(inner, vars)?.sin()),
986            SesExpr::Cos(inner) => Ok(self.evaluate_inner(inner, vars)?.cos()),
987            SesExpr::Exp(inner) => Ok(self.evaluate_inner(inner, vars)?.exp()),
988            SesExpr::Ln(inner) => {
989                let v = self.evaluate_inner(inner, vars)?;
990                if v <= 0.0 {
991                    return Err(SesError::MathDomainError(
992                        "ln of non-positive number".into(),
993                    ));
994                }
995                Ok(v.ln())
996            }
997        }
998    }
999
1000    // -----------------------------------------------------------------------
1001    // Symbolic differentiation
1002    // -----------------------------------------------------------------------
1003
1004    /// Compute the symbolic derivative of `expr` with respect to `var`.
1005    pub fn differentiate(&mut self, expr: &SesExpr, var: &str) -> SesExpr {
1006        self.stats.differentiate_calls += 1;
1007        self.diff(expr, var)
1008    }
1009
1010    fn diff(&self, expr: &SesExpr, var: &str) -> SesExpr {
1011        match expr {
1012            SesExpr::Num(_) => SesExpr::Num(0.0),
1013            SesExpr::Var(name) => {
1014                if name == var {
1015                    SesExpr::Num(1.0)
1016                } else {
1017                    SesExpr::Num(0.0)
1018                }
1019            }
1020            // (f + g)' = f' + g'
1021            SesExpr::Add(l, r) => SesExpr::add(self.diff(l, var), self.diff(r, var)),
1022            // (f - g)' = f' - g'
1023            SesExpr::Sub(l, r) => SesExpr::sub(self.diff(l, var), self.diff(r, var)),
1024            // (f * g)' = f'g + fg'
1025            SesExpr::Mul(l, r) => SesExpr::add(
1026                SesExpr::mul(self.diff(l, var), *r.clone()),
1027                SesExpr::mul(*l.clone(), self.diff(r, var)),
1028            ),
1029            // (f / g)' = (f'g - fg') / g²
1030            SesExpr::Div(l, r) => {
1031                let f_prime = self.diff(l, var);
1032                let g_prime = self.diff(r, var);
1033                let num = SesExpr::sub(
1034                    SesExpr::mul(f_prime, *r.clone()),
1035                    SesExpr::mul(*l.clone(), g_prime),
1036                );
1037                let denom = SesExpr::pow(*r.clone(), SesExpr::Num(2.0));
1038                SesExpr::div(num, denom)
1039            }
1040            // (f^g)' — general case via logarithmic differentiation
1041            // (f^g)' = f^g * (g' * ln(f) + g * f'/f)
1042            SesExpr::Pow(base, exp) => {
1043                let base_dep = self.contains_var(base, var);
1044                let exp_dep = self.contains_var(exp, var);
1045                match (base_dep, exp_dep) {
1046                    (false, false) => SesExpr::Num(0.0),
1047                    // f^n where n is constant: n * f^(n-1) * f'
1048                    (true, false) => {
1049                        let n = *exp.clone();
1050                        let n_minus_1 = SesExpr::sub(n.clone(), SesExpr::Num(1.0));
1051                        let chain = self.diff(base, var);
1052                        SesExpr::mul(
1053                            SesExpr::mul(n, SesExpr::pow(*base.clone(), n_minus_1)),
1054                            chain,
1055                        )
1056                    }
1057                    // a^g where a is constant: a^g * ln(a) * g'
1058                    (false, true) => {
1059                        let chain = self.diff(exp, var);
1060                        SesExpr::mul(
1061                            SesExpr::mul(
1062                                SesExpr::pow(*base.clone(), *exp.clone()),
1063                                SesExpr::Ln(base.clone()),
1064                            ),
1065                            chain,
1066                        )
1067                    }
1068                    // General: f^g * (g' * ln(f) + g * f'/f)
1069                    (true, true) => {
1070                        let f_prime = self.diff(base, var);
1071                        let g_prime = self.diff(exp, var);
1072                        let term1 = SesExpr::mul(g_prime, SesExpr::Ln(base.clone()));
1073                        let term2 =
1074                            SesExpr::mul(*exp.clone(), SesExpr::div(f_prime, *base.clone()));
1075                        SesExpr::mul(
1076                            SesExpr::pow(*base.clone(), *exp.clone()),
1077                            SesExpr::add(term1, term2),
1078                        )
1079                    }
1080                }
1081            }
1082            // (-f)' = -f'
1083            SesExpr::Neg(inner) => SesExpr::neg(self.diff(inner, var)),
1084            // |f|' = f * f' / |f|  (we represent it symbolically)
1085            SesExpr::Abs(inner) => {
1086                let f_prime = self.diff(inner, var);
1087                SesExpr::div(
1088                    SesExpr::mul(*inner.clone(), f_prime),
1089                    SesExpr::Abs(inner.clone()),
1090                )
1091            }
1092            // sqrt(f)' = f' / (2 * sqrt(f))
1093            SesExpr::Sqrt(inner) => {
1094                let f_prime = self.diff(inner, var);
1095                SesExpr::div(
1096                    f_prime,
1097                    SesExpr::mul(SesExpr::Num(2.0), SesExpr::Sqrt(inner.clone())),
1098                )
1099            }
1100            // sin(f)' = cos(f) * f'
1101            SesExpr::Sin(inner) => {
1102                let f_prime = self.diff(inner, var);
1103                SesExpr::mul(SesExpr::Cos(inner.clone()), f_prime)
1104            }
1105            // cos(f)' = -sin(f) * f'
1106            SesExpr::Cos(inner) => {
1107                let f_prime = self.diff(inner, var);
1108                SesExpr::neg(SesExpr::mul(SesExpr::Sin(inner.clone()), f_prime))
1109            }
1110            // exp(f)' = exp(f) * f'
1111            SesExpr::Exp(inner) => {
1112                let f_prime = self.diff(inner, var);
1113                SesExpr::mul(SesExpr::Exp(inner.clone()), f_prime)
1114            }
1115            // ln(f)' = f' / f
1116            SesExpr::Ln(inner) => {
1117                let f_prime = self.diff(inner, var);
1118                SesExpr::div(f_prime, *inner.clone())
1119            }
1120        }
1121    }
1122
1123    // -----------------------------------------------------------------------
1124    // Substitution
1125    // -----------------------------------------------------------------------
1126
1127    /// Substitute all occurrences of `var` in `expr` with `replacement`.
1128    pub fn substitute(&self, expr: &SesExpr, var: &str, replacement: &SesExpr) -> SesExpr {
1129        match expr {
1130            SesExpr::Var(name) if name == var => replacement.clone(),
1131            SesExpr::Num(_) | SesExpr::Var(_) => expr.clone(),
1132            SesExpr::Add(l, r) => SesExpr::add(
1133                self.substitute(l, var, replacement),
1134                self.substitute(r, var, replacement),
1135            ),
1136            SesExpr::Sub(l, r) => SesExpr::sub(
1137                self.substitute(l, var, replacement),
1138                self.substitute(r, var, replacement),
1139            ),
1140            SesExpr::Mul(l, r) => SesExpr::mul(
1141                self.substitute(l, var, replacement),
1142                self.substitute(r, var, replacement),
1143            ),
1144            SesExpr::Div(l, r) => SesExpr::div(
1145                self.substitute(l, var, replacement),
1146                self.substitute(r, var, replacement),
1147            ),
1148            SesExpr::Pow(b, e) => SesExpr::pow(
1149                self.substitute(b, var, replacement),
1150                self.substitute(e, var, replacement),
1151            ),
1152            SesExpr::Neg(inner) => SesExpr::neg(self.substitute(inner, var, replacement)),
1153            SesExpr::Abs(inner) => SesExpr::Abs(Box::new(self.substitute(inner, var, replacement))),
1154            SesExpr::Sqrt(inner) => {
1155                SesExpr::Sqrt(Box::new(self.substitute(inner, var, replacement)))
1156            }
1157            SesExpr::Sin(inner) => SesExpr::Sin(Box::new(self.substitute(inner, var, replacement))),
1158            SesExpr::Cos(inner) => SesExpr::Cos(Box::new(self.substitute(inner, var, replacement))),
1159            SesExpr::Exp(inner) => SesExpr::Exp(Box::new(self.substitute(inner, var, replacement))),
1160            SesExpr::Ln(inner) => SesExpr::Ln(Box::new(self.substitute(inner, var, replacement))),
1161        }
1162    }
1163
1164    // -----------------------------------------------------------------------
1165    // Variable helpers
1166    // -----------------------------------------------------------------------
1167
1168    /// Returns `true` if `expr` contains the variable `var`.
1169    pub fn contains_var(&self, expr: &SesExpr, var: &str) -> bool {
1170        match expr {
1171            SesExpr::Var(name) => name == var,
1172            SesExpr::Num(_) => false,
1173            SesExpr::Add(l, r)
1174            | SesExpr::Sub(l, r)
1175            | SesExpr::Mul(l, r)
1176            | SesExpr::Div(l, r)
1177            | SesExpr::Pow(l, r) => self.contains_var(l, var) || self.contains_var(r, var),
1178            SesExpr::Neg(i)
1179            | SesExpr::Abs(i)
1180            | SesExpr::Sqrt(i)
1181            | SesExpr::Sin(i)
1182            | SesExpr::Cos(i)
1183            | SesExpr::Exp(i)
1184            | SesExpr::Ln(i) => self.contains_var(i, var),
1185        }
1186    }
1187
1188    /// Collect all distinct variable names appearing in `expr`.
1189    pub fn collect_vars(&self, expr: &SesExpr) -> HashSet<String> {
1190        let mut set = HashSet::new();
1191        collect_vars_inner(expr, &mut set);
1192        set
1193    }
1194
1195    // -----------------------------------------------------------------------
1196    // String conversion
1197    // -----------------------------------------------------------------------
1198
1199    /// Convert `expr` to infix string notation with minimal parentheses.
1200    pub fn to_string(&self, expr: &SesExpr) -> String {
1201        expr.to_string()
1202    }
1203
1204    // -----------------------------------------------------------------------
1205    // Parser
1206    // -----------------------------------------------------------------------
1207
1208    /// Parse an infix expression string into a `SesExpr`.
1209    ///
1210    /// Supports: numbers, identifiers (variables), `+`, `-`, `*`, `/`, `^`,
1211    /// unary `-`, and function calls `sin`, `cos`, `exp`, `ln`, `sqrt`, `abs`.
1212    /// Parentheses and `|...|` for absolute value are also supported.
1213    pub fn parse(s: &str) -> Result<SesExpr, SesError> {
1214        let tokens = tokenize(s)?;
1215        let mut parser = Parser::new(tokens);
1216        let expr = parser.parse_expr()?;
1217        if !parser.is_at_end() {
1218            return Err(SesError::ParseError(format!(
1219                "unexpected token at position {}",
1220                parser.pos
1221            )));
1222        }
1223        Ok(expr)
1224    }
1225
1226    // -----------------------------------------------------------------------
1227    // Stats
1228    // -----------------------------------------------------------------------
1229
1230    /// Return current simplifier statistics.
1231    pub fn simplifier_stats(&self) -> SesSimplifierStats {
1232        let mut stats = self.stats.clone();
1233        stats.history_len = self.history.len();
1234        stats
1235    }
1236
1237    /// Return a reference to the simplification history.
1238    pub fn history(&self) -> &VecDeque<SesSimplificationStep> {
1239        &self.history
1240    }
1241
1242    /// Clear the simplification history.
1243    pub fn clear_history(&mut self) {
1244        self.history.clear();
1245        self.stats.history_len = 0;
1246    }
1247
1248    /// Return a reference to the current configuration.
1249    pub fn config(&self) -> &SesSimplifierConfig {
1250        &self.config
1251    }
1252
1253    /// Return number of registered rules.
1254    pub fn rule_count(&self) -> usize {
1255        self.rules.len()
1256    }
1257}
1258
1259impl Default for SymbolicExpressionSimplifier {
1260    fn default() -> Self {
1261        Self::new()
1262    }
1263}
1264
1265// ---------------------------------------------------------------------------
1266// Helper predicates
1267// ---------------------------------------------------------------------------
1268
1269fn is_zero(e: &SesExpr) -> bool {
1270    matches!(e, SesExpr::Num(v) if *v == 0.0)
1271}
1272
1273fn is_one(e: &SesExpr) -> bool {
1274    matches!(e, SesExpr::Num(v) if *v == 1.0)
1275}
1276
1277fn is_two(e: &SesExpr) -> bool {
1278    matches!(e, SesExpr::Num(v) if *v == 2.0)
1279}
1280
1281/// Returns `true` if `expr` contains no variables (fully constant).
1282fn is_constant(e: &SesExpr) -> bool {
1283    match e {
1284        SesExpr::Num(_) => true,
1285        SesExpr::Var(_) => false,
1286        SesExpr::Add(l, r)
1287        | SesExpr::Sub(l, r)
1288        | SesExpr::Mul(l, r)
1289        | SesExpr::Div(l, r)
1290        | SesExpr::Pow(l, r) => is_constant(l) && is_constant(r),
1291        SesExpr::Neg(i)
1292        | SesExpr::Abs(i)
1293        | SesExpr::Sqrt(i)
1294        | SesExpr::Sin(i)
1295        | SesExpr::Cos(i)
1296        | SesExpr::Exp(i)
1297        | SesExpr::Ln(i) => is_constant(i),
1298    }
1299}
1300
1301/// Structural equality of two expressions (sufficient for rewrite rules).
1302fn exprs_equal(a: &SesExpr, b: &SesExpr) -> bool {
1303    a == b
1304}
1305
1306/// Check if `e` is of the form `sin(x)^2 + cos(x)^2`.
1307fn is_sin2_plus_cos2(e: &SesExpr) -> bool {
1308    let SesExpr::Add(l, r) = e else {
1309        return false;
1310    };
1311    is_sin_squared(l) && is_cos_squared(r) || is_cos_squared(l) && is_sin_squared(r)
1312}
1313
1314fn is_sin_squared(e: &SesExpr) -> bool {
1315    if let SesExpr::Pow(base, exp) = e {
1316        is_two(exp) && matches!(base.as_ref(), SesExpr::Sin(_))
1317    } else {
1318        false
1319    }
1320}
1321
1322fn is_cos_squared(e: &SesExpr) -> bool {
1323    if let SesExpr::Pow(base, exp) = e {
1324        is_two(exp) && matches!(base.as_ref(), SesExpr::Cos(_))
1325    } else {
1326        false
1327    }
1328}
1329
1330fn collect_vars_inner(e: &SesExpr, set: &mut HashSet<String>) {
1331    match e {
1332        SesExpr::Var(name) => {
1333            set.insert(name.clone());
1334        }
1335        SesExpr::Num(_) => {}
1336        SesExpr::Add(l, r)
1337        | SesExpr::Sub(l, r)
1338        | SesExpr::Mul(l, r)
1339        | SesExpr::Div(l, r)
1340        | SesExpr::Pow(l, r) => {
1341            collect_vars_inner(l, set);
1342            collect_vars_inner(r, set);
1343        }
1344        SesExpr::Neg(i)
1345        | SesExpr::Abs(i)
1346        | SesExpr::Sqrt(i)
1347        | SesExpr::Sin(i)
1348        | SesExpr::Cos(i)
1349        | SesExpr::Exp(i)
1350        | SesExpr::Ln(i) => collect_vars_inner(i, set),
1351    }
1352}
1353
1354// ---------------------------------------------------------------------------
1355// Tokenizer
1356// ---------------------------------------------------------------------------
1357
1358#[derive(Debug, Clone, PartialEq)]
1359enum Token {
1360    Num(f64),
1361    Ident(String),
1362    Plus,
1363    Minus,
1364    Star,
1365    Slash,
1366    Caret,
1367    LParen,
1368    RParen,
1369    Pipe,
1370}
1371
1372fn tokenize(s: &str) -> Result<Vec<Token>, SesError> {
1373    let mut tokens = Vec::new();
1374    let chars: Vec<char> = s.chars().collect();
1375    let mut i = 0;
1376    while i < chars.len() {
1377        let c = chars[i];
1378        if c.is_whitespace() {
1379            i += 1;
1380            continue;
1381        }
1382        if c.is_ascii_digit() || (c == '.' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit())
1383        {
1384            let start = i;
1385            while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
1386                i += 1;
1387            }
1388            // Allow 'e'/'E' for scientific notation
1389            if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
1390                i += 1;
1391                if i < chars.len() && (chars[i] == '+' || chars[i] == '-') {
1392                    i += 1;
1393                }
1394                while i < chars.len() && chars[i].is_ascii_digit() {
1395                    i += 1;
1396                }
1397            }
1398            let num_str: String = chars[start..i].iter().collect();
1399            let v = num_str
1400                .parse::<f64>()
1401                .map_err(|_| SesError::ParseError(format!("invalid number: {num_str}")))?;
1402            tokens.push(Token::Num(v));
1403        } else if c.is_alphabetic() || c == '_' {
1404            let start = i;
1405            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
1406                i += 1;
1407            }
1408            let ident: String = chars[start..i].iter().collect();
1409            tokens.push(Token::Ident(ident));
1410        } else {
1411            let tok = match c {
1412                '+' => Token::Plus,
1413                '-' => Token::Minus,
1414                '*' => Token::Star,
1415                '/' => Token::Slash,
1416                '^' => Token::Caret,
1417                '(' => Token::LParen,
1418                ')' => Token::RParen,
1419                '|' => Token::Pipe,
1420                _ => return Err(SesError::ParseError(format!("unexpected character: '{c}'"))),
1421            };
1422            tokens.push(tok);
1423            i += 1;
1424        }
1425    }
1426    Ok(tokens)
1427}
1428
1429// ---------------------------------------------------------------------------
1430// Recursive-descent parser
1431// ---------------------------------------------------------------------------
1432
1433struct Parser {
1434    tokens: Vec<Token>,
1435    pos: usize,
1436}
1437
1438impl Parser {
1439    fn new(tokens: Vec<Token>) -> Self {
1440        Parser { tokens, pos: 0 }
1441    }
1442
1443    fn peek(&self) -> Option<&Token> {
1444        self.tokens.get(self.pos)
1445    }
1446
1447    fn consume(&mut self) -> Option<Token> {
1448        if self.pos < self.tokens.len() {
1449            let t = self.tokens[self.pos].clone();
1450            self.pos += 1;
1451            Some(t)
1452        } else {
1453            None
1454        }
1455    }
1456
1457    fn expect(&mut self, expected: &Token) -> Result<(), SesError> {
1458        match self.consume() {
1459            Some(ref t) if t == expected => Ok(()),
1460            Some(t) => Err(SesError::ParseError(format!(
1461                "expected {expected:?}, got {t:?}"
1462            ))),
1463            None => Err(SesError::ParseError(format!(
1464                "expected {expected:?}, got EOF"
1465            ))),
1466        }
1467    }
1468
1469    fn is_at_end(&self) -> bool {
1470        self.pos >= self.tokens.len()
1471    }
1472
1473    /// expr = additive
1474    fn parse_expr(&mut self) -> Result<SesExpr, SesError> {
1475        self.parse_additive()
1476    }
1477
1478    /// additive = multiplicative (('+' | '-') multiplicative)*
1479    fn parse_additive(&mut self) -> Result<SesExpr, SesError> {
1480        let mut lhs = self.parse_multiplicative()?;
1481        loop {
1482            match self.peek() {
1483                Some(Token::Plus) => {
1484                    self.consume();
1485                    let rhs = self.parse_multiplicative()?;
1486                    lhs = SesExpr::add(lhs, rhs);
1487                }
1488                Some(Token::Minus) => {
1489                    self.consume();
1490                    let rhs = self.parse_multiplicative()?;
1491                    lhs = SesExpr::sub(lhs, rhs);
1492                }
1493                _ => break,
1494            }
1495        }
1496        Ok(lhs)
1497    }
1498
1499    /// multiplicative = power (('*' | '/') power)*
1500    fn parse_multiplicative(&mut self) -> Result<SesExpr, SesError> {
1501        let mut lhs = self.parse_power()?;
1502        loop {
1503            match self.peek() {
1504                Some(Token::Star) => {
1505                    self.consume();
1506                    let rhs = self.parse_power()?;
1507                    lhs = SesExpr::mul(lhs, rhs);
1508                }
1509                Some(Token::Slash) => {
1510                    self.consume();
1511                    let rhs = self.parse_power()?;
1512                    lhs = SesExpr::div(lhs, rhs);
1513                }
1514                _ => break,
1515            }
1516        }
1517        Ok(lhs)
1518    }
1519
1520    /// power = unary ('^' power)?   (right-associative)
1521    fn parse_power(&mut self) -> Result<SesExpr, SesError> {
1522        let base = self.parse_unary()?;
1523        if matches!(self.peek(), Some(Token::Caret)) {
1524            self.consume();
1525            let exp = self.parse_power()?; // right-associative
1526            Ok(SesExpr::pow(base, exp))
1527        } else {
1528            Ok(base)
1529        }
1530    }
1531
1532    /// unary = '-' unary | atom
1533    fn parse_unary(&mut self) -> Result<SesExpr, SesError> {
1534        if matches!(self.peek(), Some(Token::Minus)) {
1535            self.consume();
1536            let inner = self.parse_unary()?;
1537            Ok(SesExpr::neg(inner))
1538        } else {
1539            self.parse_atom()
1540        }
1541    }
1542
1543    /// atom = num | ident | ident '(' expr ')' | '(' expr ')' | '|' expr '|'
1544    fn parse_atom(&mut self) -> Result<SesExpr, SesError> {
1545        match self.peek().cloned() {
1546            Some(Token::Num(v)) => {
1547                self.consume();
1548                Ok(SesExpr::Num(v))
1549            }
1550            Some(Token::Ident(name)) => {
1551                self.consume();
1552                // Function call?
1553                if matches!(self.peek(), Some(Token::LParen)) {
1554                    self.consume(); // consume '('
1555                    let arg = self.parse_expr()?;
1556                    self.expect(&Token::RParen)?;
1557                    match name.to_lowercase().as_str() {
1558                        "sin" => Ok(SesExpr::Sin(Box::new(arg))),
1559                        "cos" => Ok(SesExpr::Cos(Box::new(arg))),
1560                        "exp" => Ok(SesExpr::Exp(Box::new(arg))),
1561                        "ln" | "log" => Ok(SesExpr::Ln(Box::new(arg))),
1562                        "sqrt" => Ok(SesExpr::Sqrt(Box::new(arg))),
1563                        "abs" => Ok(SesExpr::Abs(Box::new(arg))),
1564                        "neg" => Ok(SesExpr::neg(arg)),
1565                        other => Err(SesError::ParseError(format!("unknown function: {other}"))),
1566                    }
1567                } else {
1568                    Ok(SesExpr::Var(name))
1569                }
1570            }
1571            Some(Token::LParen) => {
1572                self.consume();
1573                let inner = self.parse_expr()?;
1574                self.expect(&Token::RParen)?;
1575                Ok(inner)
1576            }
1577            Some(Token::Pipe) => {
1578                self.consume();
1579                let inner = self.parse_expr()?;
1580                self.expect(&Token::Pipe)?;
1581                Ok(SesExpr::Abs(Box::new(inner)))
1582            }
1583            Some(t) => Err(SesError::ParseError(format!("unexpected token: {t:?}"))),
1584            None => Err(SesError::ParseError("unexpected end of input".into())),
1585        }
1586    }
1587}
1588
1589// ---------------------------------------------------------------------------
1590// Tests — split into ses_tests.rs to keep this file under 2000 lines.
1591// ---------------------------------------------------------------------------
1592
1593#[cfg(test)]
1594#[path = "ses_tests.rs"]
1595mod tests;