Skip to main content

symplex/api/
expr.rs

1//! User-facing expression handle.
2//!
3//! [`Ex`] is a lightweight handle to a symbolic expression. It holds a
4//! reference-counted pointer to the shared `ContextInner` (which
5//! contains both the arena and the assumption cache) plus an expression ID.
6//! Clone is cheap (~5ns, just an Arc clone + u32 copy).
7//!
8//! `Ex` is **not** `Copy` because it contains an `Arc`. This is a
9//! deliberate trade-off: storing the context pointer means every method
10//! (`pow`, `sin`, `is_positive`, operators, `Display`) works without
11//! requiring any special scoping.
12//!
13//! To reduce clone noise, all binary operators are implemented for every
14//! combination of `Ex` and `&Ex`, and for `i64` on both sides.
15//!
16//! # Examples
17//!
18//! ```
19//! use symplex::prelude::*;
20//!
21//! let ctx = Context::new();
22//! let x = ctx.symbol("x");
23//! let expr = &x * &x + &x * 2 + 1;
24//! assert_eq!(format!("{expr}"), "x^2 + 2*x + 1");
25//! ```
26//!
27//! Methods are chainable:
28//!
29//! ```
30//! use symplex::prelude::*;
31//!
32//! let ctx = Context::new();
33//! let x = ctx.symbol("x");
34//! let expr = x.powi(2).sin();
35//! assert_eq!(format!("{expr}"), "sin(x^2)");
36//! ```
37//!
38//! ```
39//! use symplex::prelude::*;
40//!
41//! let ctx = Context::new();
42//! let x = ctx.symbol("x");
43//! let expr = x.sin();
44//! assert_eq!(format!("{expr}"), "sin(x)");
45//! ```
46
47use std::marker::PhantomData;
48use std::sync::Arc;
49
50use parking_lot::RwLock;
51use tracing::debug_span;
52
53use crate::api::context::ContextInner;
54use crate::base::errors::SymplexError;
55use crate::base::node::{CtxId, ExprId};
56
57/// Options controlling [`Expr::simplify_with`].
58pub use crate::simplify::simplify_engine::SimplifyOpts;
59
60// ═══════════════════════════════════════════════════════════════════════════
61// Sort system — compile-time expression typing
62// ═══════════════════════════════════════════════════════════════════════════
63
64/// Marker trait for expression sorts.
65///
66/// Sorts distinguish numeric expressions (which support arithmetic,
67/// calculus, etc.) from boolean expressions (comparisons, logic)
68/// at compile time. The sort is carried as a phantom type parameter
69/// on [`Expr`] and has zero runtime cost.
70pub trait Sort: 'static + Clone + Send + Sync {}
71
72/// Numeric sort: real, complex, and integer values.
73/// Supports arithmetic, calculus, and algebraic operations.
74#[derive(Clone)]
75pub struct Numeric;
76
77/// Boolean sort: true/false values from comparisons and logic.
78/// Supports `and`, `or`, `not` operations.
79#[derive(Clone)]
80pub struct Boolean;
81
82/// Set-valued sort: intervals, finite sets, unions.
83/// Supports set operations (union, intersection, complement).
84#[derive(Clone)]
85pub struct SetValued;
86
87impl Sort for Numeric {}
88impl Sort for Boolean {}
89impl Sort for SetValued {}
90
91/// Structural classification of an expression node.
92///
93/// Returned by [`Expr::expr_type()`]. This collapses the internal
94/// `ExprNode` enum (over 70 variants) into a user-friendly classification.
95///
96/// # Examples
97///
98/// ```
99/// use symplex::prelude::*;
100/// use symplex::expr::ExprType;
101///
102/// let ctx = Context::new();
103/// let x = ctx.symbol("x");
104/// assert_eq!(x.expr_type(), ExprType::Symbol);
105/// assert_eq!((&x + 1).expr_type(), ExprType::Add);
106/// assert_eq!(x.sin().expr_type(), ExprType::Function);
107/// assert_eq!(ctx.int(42).expr_type(), ExprType::Number);
108/// ```
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum ExprType {
111    /// A numeric literal (integer or rational).
112    Number,
113    /// A symbolic variable.
114    Symbol,
115    /// A mathematical constant (π, e, i, ∞, etc.).
116    Constant,
117    /// An n-ary sum.
118    Add,
119    /// An n-ary product.
120    Mul,
121    /// Exponentiation.
122    Pow,
123    /// Unary negation.
124    Neg,
125    /// A mathematical function (sin, cos, ln, exp, abs, etc.).
126    Function,
127    /// Application of a user-defined function.
128    Apply,
129    /// A formal derivative.
130    Derivative,
131    /// A formal integral.
132    Integral,
133    /// A set expression (interval, finite set, union, intersection, complement).
134    Set,
135    /// A formal/unevaluated computation (DefiniteIntegral, Limit, Series,
136    /// LaplaceTransform, etc.)
137    ///
138    /// Every expression of this type also reports
139    /// [`has_unevaluated`](Expr::has_unevaluated).  `RootOf`/`RootSum` are
140    /// *not* unevaluated: they are exact algebraic values and classify as
141    /// [`Constant`](Self::Constant) (numeric polynomial) or
142    /// [`Function`](Self::Function) (parametric polynomial).
143    Unevaluated,
144}
145
146/// A symbolic expression handle, parameterized by sort.
147///
148/// `Expr<Numeric>` (aliased as [`Ex`]) represents numeric expressions.
149/// `Expr<Boolean>` (aliased as [`BoolEx`]) represents boolean expressions.
150///
151/// The sort parameter is a phantom type — zero runtime cost.
152/// It prevents invalid operations at compile time:
153/// - `sin(bool_expr)` won't compile (sin is only on `Expr<Numeric>`)
154/// - `bool_expr + 1` won't compile (Add is only on `Expr<Numeric>`)
155/// - `numeric.and(other)` won't compile (and is only on `Expr<Boolean>`)
156#[derive(Clone)]
157pub struct Expr<S: Sort> {
158    pub(crate) ctx_id: CtxId,
159    pub(crate) inner: Arc<RwLock<ContextInner>>,
160    id: ExprId,
161    pub(crate) _sort: PhantomData<S>,
162}
163
164/// A numeric expression — the primary type for symbolic math.
165pub type Ex = Expr<Numeric>;
166
167impl AsRef<Ex> for Ex {
168    #[inline]
169    fn as_ref(&self) -> &Ex {
170        self
171    }
172}
173
174/// A boolean expression — comparisons and logical operations.
175pub type BoolEx = Expr<Boolean>;
176
177/// A set-valued expression — intervals, finite sets, unions.
178pub type SetEx = Expr<SetValued>;
179
180// ═══════════════════════════════════════════════════════════════════════════
181// impl<S: Sort> Expr<S> — wrap helpers
182// ═══════════════════════════════════════════════════════════════════════════
183
184impl<S: Sort> Expr<S> {
185    /// Construct an `Expr` from raw parts.
186    ///
187    /// The caller must guarantee that `id` is a valid [`ExprId`] in the
188    /// arena behind `inner`.  This is the **only** constructor — all other
189    /// modules must go through it because the `id` field is private.
190    #[inline]
191    pub(crate) fn from_raw_parts(
192        ctx_id: CtxId,
193        inner: Arc<RwLock<ContextInner>>,
194        id: ExprId,
195    ) -> Self {
196        Expr {
197            ctx_id,
198            inner,
199            id,
200            _sort: PhantomData,
201        }
202    }
203
204    /// Helper — build a new Expr of the SAME sort from the same context.
205    #[inline]
206    pub(crate) fn wrap(&self, id: ExprId) -> Expr<S> {
207        Expr::from_raw_parts(self.ctx_id, Arc::clone(&self.inner), id)
208    }
209
210    /// Helper — build a new Expr of a DIFFERENT sort from the same context.
211    #[inline]
212    pub(crate) fn wrap_as<T: Sort>(&self, id: ExprId) -> Expr<T> {
213        Expr::from_raw_parts(self.ctx_id, Arc::clone(&self.inner), id)
214    }
215
216    /// Return this expression's arena index.
217    ///
218    /// This is always safe — you are accessing your own data within
219    /// your own context's arena.
220    #[inline]
221    pub(crate) fn raw_id(&self) -> ExprId {
222        self.id
223    }
224
225    /// Return another expression's arena index after verifying it belongs
226    /// to the same [`Context`](crate::api::context::Context) as `self`.
227    ///
228    /// # Panics
229    ///
230    /// Panics with a descriptive message if `self` and `other` belong to
231    /// different contexts.  This prevents silent data corruption from
232    /// cross-context [`ExprId`] misuse.
233    #[inline]
234    pub(crate) fn checked_id<T: Sort>(&self, other: &Expr<T>) -> ExprId {
235        if self.ctx_id != other.ctx_id {
236            panic!(
237                "symplex: cannot combine expressions from different contexts \
238                 (context {} and context {}). All expressions in an operation \
239                 must originate from the same Context.",
240                self.ctx_id.0, other.ctx_id.0
241            );
242        }
243        other.id
244    }
245
246    /// Returns a [`Context`](crate::api::context::Context) handle that
247    /// shares this expression's arena and assumption cache.
248    ///
249    /// Useful when you need to create new expressions (constants, rationals)
250    /// guaranteed to live in the same context as an existing expression.
251    #[must_use]
252    pub fn context(&self) -> crate::api::context::Context {
253        crate::api::context::Context {
254            id: self.ctx_id,
255            inner: Arc::clone(&self.inner),
256        }
257    }
258}
259
260// ═══════════════════════════════════════════════════════════════════════════
261// impl<S: Sort> Expr<S> — Common (sort-preserving) methods
262// ═══════════════════════════════════════════════════════════════════════════
263
264impl<S: Sort> Expr<S> {
265    /// Returns the raw `ExprId` inside this handle.
266    ///
267    /// **Note:** This is an opaque arena-local index.  It is only
268    /// meaningful within the [`Context`](crate::api::context::Context)
269    /// that created this expression.  Comparing `ExprId` values across
270    /// contexts is undefined.
271    #[inline]
272    pub fn id(&self) -> ExprId {
273        self.id
274    }
275
276    /// Returns the `CtxId` that this expression belongs to.
277    #[inline]
278    pub fn ctx_id(&self) -> CtxId {
279        self.ctx_id
280    }
281
282    // ── Structural predicates ──────────────────────────────────────
283
284    /// Returns `true` if this expression is structurally zero (O(1)).
285    #[must_use]
286    pub fn is_zero_structural(&self) -> bool {
287        self.inner.read().arena.is_zero_structural(self.id)
288    }
289
290    /// Returns `true` if this expression is structurally one (O(1)).
291    #[must_use]
292    pub fn is_one_structural(&self) -> bool {
293        self.inner.read().arena.is_one_structural(self.id)
294    }
295
296    // ── Structural introspection ───────────────────────────────────
297
298    /// Returns the set of free symbols in this expression.
299    ///
300    /// Each symbol appears at most once. The order is deterministic
301    /// but unspecified.
302    ///
303    /// Symbols are always numeric, so this returns `Vec<Ex>` regardless
304    /// of the sort of `self`.
305    #[must_use]
306    pub fn free_symbols(&self) -> Vec<Ex> {
307        let inner = self.inner.read();
308        let expr_ids = crate::base::walk::free_symbols(&inner.arena, self.id);
309        drop(inner);
310        expr_ids
311            .into_iter()
312            .map(|eid| self.wrap_as::<Numeric>(eid))
313            .collect()
314    }
315
316    /// Returns `true` if this expression contains any unevaluated formal
317    /// nodes such as `Integral(...)`, `Derivative(...)`, `Limit(...)`, etc.
318    ///
319    /// Useful for checking whether a symbolic computation fully evaluated
320    /// or left formal/unevaluated placeholders.
321    #[must_use]
322    pub fn has_unevaluated(&self) -> bool {
323        let inner = self.inner.read();
324        crate::base::walk::has_unevaluated(&inner.arena, self.raw_id())
325    }
326
327    /// Count the number of operations (non-atom nodes) in this expression.
328    ///
329    /// Atoms (numbers, symbols, constants) count as 0.
330    /// Each operator or function application counts as 1.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use symplex::prelude::*;
336    ///
337    /// let ctx = Context::new();
338    /// let x = ctx.symbol("x");
339    /// assert_eq!(x.count_ops(), 0);           // atom
340    /// assert_eq!((&x + 1).count_ops(), 1);    // one Add
341    /// assert_eq!(x.sin().powi(2).count_ops(), 2); // Sin + Pow
342    /// ```
343    #[must_use]
344    pub fn count_ops(&self) -> usize {
345        let inner = self.inner.read();
346        inner.arena.count_ops(self.id)
347    }
348
349    /// Returns the number of top-level terms in this expression.
350    ///
351    /// For an `Add` node, returns the number of summands.
352    /// For anything else, returns 1.
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use symplex::prelude::*;
358    ///
359    /// let ctx = Context::new();
360    /// let x = ctx.symbol("x");
361    /// assert_eq!((&x + 1).term_count(), 2);
362    /// assert_eq!(x.powi(2).term_count(), 1);
363    /// ```
364    #[must_use]
365    pub fn term_count(&self) -> usize {
366        let inner = self.inner.read();
367        match inner.arena.node(self.id) {
368            crate::base::node::ExprNode::Add(children) => children.len(),
369            _ => 1,
370        }
371    }
372
373    /// Returns the direct children (arguments) of this expression.
374    ///
375    /// - For `Add`: returns the summands.
376    /// - For `Mul`: returns the factors.
377    /// - For `Pow`: returns `[base, exponent]`.
378    /// - For `Neg`: returns `[inner]`.
379    /// - For functions (sin, cos, etc.): returns `[argument]`.
380    /// - For atoms (numbers, symbols, constants): returns `[]`.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use symplex::prelude::*;
386    ///
387    /// let ctx = Context::new();
388    /// let x = ctx.symbol("x");
389    /// let expr = &x + 1;
390    /// let children = expr.args();
391    /// assert_eq!(children.len(), 2);
392    /// ```
393    #[must_use]
394    pub fn args(&self) -> Vec<Expr<S>> {
395        let inner = self.inner.read();
396        let child_ids = inner.arena.node(self.id).children();
397        child_ids.iter().map(|&id| self.wrap(id)).collect()
398    }
399
400    /// Returns the structural type of this expression.
401    ///
402    /// Collapses the internal `ExprNode` enum (over 70 variants) into a
403    /// user-friendly [`ExprType`] classification.
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// use symplex::prelude::*;
409    /// use symplex::expr::ExprType;
410    ///
411    /// let ctx = Context::new();
412    /// let x = ctx.symbol("x");
413    /// assert_eq!(x.expr_type(), ExprType::Symbol);
414    /// assert_eq!(x.sin().expr_type(), ExprType::Function);
415    /// assert_eq!((&x + 1).expr_type(), ExprType::Add);
416    /// ```
417    #[must_use]
418    pub fn expr_type(&self) -> ExprType {
419        let inner = self.inner.read();
420        match inner.arena.node(self.id) {
421            crate::base::node::ExprNode::Num(_) => ExprType::Number,
422            crate::base::node::ExprNode::Symbol(_) => ExprType::Symbol,
423            crate::base::node::ExprNode::Pi
424            | crate::base::node::ExprNode::E
425            | crate::base::node::ExprNode::ImaginaryUnit
426            | crate::base::node::ExprNode::EulerGamma
427            | crate::base::node::ExprNode::Catalan
428            | crate::base::node::ExprNode::GoldenRatio
429            | crate::base::node::ExprNode::PhysicalConstant(_, _)
430            | crate::base::node::ExprNode::Infinity
431            | crate::base::node::ExprNode::NegInfinity
432            | crate::base::node::ExprNode::ComplexInfinity
433            | crate::base::node::ExprNode::NaN => ExprType::Constant,
434            crate::base::node::ExprNode::Add(_) => ExprType::Add,
435            crate::base::node::ExprNode::Mul(_) => ExprType::Mul,
436            crate::base::node::ExprNode::Pow(_, _) => ExprType::Pow,
437            crate::base::node::ExprNode::Neg(_) => ExprType::Neg,
438            crate::base::node::ExprNode::Sin(_)
439            | crate::base::node::ExprNode::Cos(_)
440            | crate::base::node::ExprNode::Tan(_)
441            | crate::base::node::ExprNode::Exp(_)
442            | crate::base::node::ExprNode::Ln(_)
443            | crate::base::node::ExprNode::Abs(_)
444            | crate::base::node::ExprNode::Asin(_)
445            | crate::base::node::ExprNode::Acos(_)
446            | crate::base::node::ExprNode::Atan(_)
447            | crate::base::node::ExprNode::Atan2(_, _)
448            | crate::base::node::ExprNode::Sinh(_)
449            | crate::base::node::ExprNode::Cosh(_)
450            | crate::base::node::ExprNode::Tanh(_)
451            | crate::base::node::ExprNode::Asinh(_)
452            | crate::base::node::ExprNode::Acosh(_)
453            | crate::base::node::ExprNode::Atanh(_)
454            | crate::base::node::ExprNode::Sign(_)
455            | crate::base::node::ExprNode::Floor(_)
456            | crate::base::node::ExprNode::Ceiling(_)
457            | crate::base::node::ExprNode::Min(_)
458            | crate::base::node::ExprNode::Max(_)
459            | crate::base::node::ExprNode::Sum(_, _, _, _)
460            | crate::base::node::ExprNode::Product_(_, _, _, _) => ExprType::Function,
461            crate::base::node::ExprNode::Apply(_, _) => ExprType::Apply,
462            crate::base::node::ExprNode::Derivative(_, _) => ExprType::Derivative,
463            crate::base::node::ExprNode::Integral(_, _) => ExprType::Integral,
464            crate::base::node::ExprNode::Factorial(_)
465            | crate::base::node::ExprNode::Binomial(_, _)
466            | crate::base::node::ExprNode::Gamma(_)
467            | crate::base::node::ExprNode::LogGamma(_)
468            | crate::base::node::ExprNode::Digamma(_)
469            | crate::base::node::ExprNode::Erf(_)
470            | crate::base::node::ExprNode::Erfc(_)
471            | crate::base::node::ExprNode::LambertW(_)
472            | crate::base::node::ExprNode::Beta(_, _)
473            | crate::base::node::ExprNode::Re(_)
474            | crate::base::node::ExprNode::Im(_)
475            | crate::base::node::ExprNode::Conjugate(_)
476            | crate::base::node::ExprNode::Arg(_)
477            | crate::base::node::ExprNode::Si(_)
478            | crate::base::node::ExprNode::Ci(_)
479            | crate::base::node::ExprNode::Ei(_)
480            | crate::base::node::ExprNode::Li(_)
481            | crate::base::node::ExprNode::Zeta(_)
482            | crate::base::node::ExprNode::Polygamma(_, _)
483            | crate::base::node::ExprNode::KroneckerDelta(_, _) => ExprType::Function,
484            crate::base::node::ExprNode::BoolTrue | crate::base::node::ExprNode::BoolFalse => {
485                ExprType::Constant
486            }
487            crate::base::node::ExprNode::Gt(_, _)
488            | crate::base::node::ExprNode::Ge(_, _)
489            | crate::base::node::ExprNode::Eq_(_, _)
490            | crate::base::node::ExprNode::Ne(_, _)
491            | crate::base::node::ExprNode::And(_)
492            | crate::base::node::ExprNode::Or(_)
493            | crate::base::node::ExprNode::Not(_)
494            | crate::base::node::ExprNode::Piecewise(_)
495            | crate::base::node::ExprNode::Heaviside(_)
496            | crate::base::node::ExprNode::DiracDelta(_) => ExprType::Function,
497            crate::base::node::ExprNode::EmptySet
498            | crate::base::node::ExprNode::UniversalSet
499            | crate::base::node::ExprNode::Interval(_, _, _)
500            | crate::base::node::ExprNode::FiniteSet(_)
501            | crate::base::node::ExprNode::SetUnion(_)
502            | crate::base::node::ExprNode::SetIntersection(_)
503            | crate::base::node::ExprNode::SetComplement(_, _) => ExprType::Set,
504            // `RootOf`/`RootSum` are complete algebraic values, not pending
505            // computations (consistent with `has_unevaluated`, which does not
506            // report them): a constant when the polynomial has numeric
507            // coefficients, otherwise a function of its parameters.
508            crate::base::node::ExprNode::RootOf(..) | crate::base::node::ExprNode::RootSum(..) => {
509                if crate::base::walk::free_symbols(&inner.arena, self.id).is_empty() {
510                    ExprType::Constant
511                } else {
512                    ExprType::Function
513                }
514            }
515            crate::base::node::ExprNode::DefiniteIntegral(..)
516            | crate::base::node::ExprNode::Limit(..)
517            | crate::base::node::ExprNode::Series(..)
518            | crate::base::node::ExprNode::LaplaceTransform(..)
519            | crate::base::node::ExprNode::InverseLaplaceTransform(..)
520            | crate::base::node::ExprNode::Residue(..)
521            | crate::base::node::ExprNode::DSolve(..)
522            | crate::base::node::ExprNode::ConditionSet(..) => ExprType::Unevaluated,
523        }
524    }
525
526    // ── Substitution ───────────────────────────────────────────────
527
528    /// Structural substitution: replace every occurrence of `old` with `new`.
529    ///
530    /// This is **structural** — only exact node matches are replaced.
531    /// `(1/x).subs(x², 1)` returns `1/x` unchanged because `x²` does
532    /// not appear as a node in `x⁻¹`.
533    ///
534    /// The result is re-canonicalized, so like-term collection and
535    /// other invariants are maintained.
536    ///
537    /// Returns `self` unchanged (same `Expr`) if `old` does not appear.
538    #[must_use = "returns a new expression with substitutions applied"]
539    pub fn subs(&self, old: &Ex, new: &Ex) -> Expr<S> {
540        let old_id = self.checked_id(old);
541        let new_id = self.checked_id(new);
542        let id = self
543            .inner
544            .write()
545            .arena
546            .subs_structural(self.id, old_id, new_id);
547        self.wrap(id)
548    }
549
550    /// Substitute a symbol with an integer value.
551    ///
552    /// Convenience shorthand for `self.subs(old, &ctx.int(n))` that
553    /// avoids needing to construct the integer expression manually.
554    ///
555    /// # Examples
556    ///
557    /// ```
558    /// use symplex::prelude::*;
559    ///
560    /// let ctx = Context::new();
561    /// let x = ctx.symbol("x");
562    /// let expr = x.powi(2);
563    /// let result = expr.subs_i64(&x, 3);
564    /// assert_eq!(format!("{result}"), "9");
565    /// ```
566    #[must_use = "returns a new expression with substitutions applied"]
567    pub fn subs_i64(&self, old: &Ex, new: i64) -> Expr<S> {
568        let old_id = self.checked_id(old);
569        let mut inner = self.inner.write();
570        let new_id = inner.arena.int(new);
571        let id = inner.arena.subs_structural(self.id, old_id, new_id);
572        drop(inner);
573        self.wrap(id)
574    }
575
576    /// Simultaneous substitution of multiple `(old, new)` pairs.
577    ///
578    /// All replacements happen "at once" — earlier substitutions do
579    /// not affect later ones.
580    #[must_use = "returns a new expression with substitutions applied"]
581    pub fn subs_map(&self, replacements: &[(&Ex, &Ex)]) -> Expr<S> {
582        let pairs: smallvec::SmallVec<[(crate::base::node::ExprId, crate::base::node::ExprId); 4]> =
583            replacements
584                .iter()
585                .map(|(o, n)| (self.checked_id(o), self.checked_id(n)))
586                .collect();
587        let id = self
588            .inner
589            .write()
590            .arena
591            .subs_map_structural(self.id, &pairs);
592        self.wrap(id)
593    }
594
595    // ── Transformations ────────────────────────────────────────────
596
597    /// Algebraic expansion (distribute products over sums, expand
598    /// integer powers of sums).
599    ///
600    /// - `a * (b + c)` → `a*b + a*c`
601    /// - `(a + b)^n` → multinomial expansion
602    ///
603    /// Does NOT evaluate functions, factor, or simplify.
604    ///
605    /// # Examples
606    ///
607    /// ```
608    /// use symplex::prelude::*;
609    ///
610    /// let ctx = Context::new();
611    /// let x = ctx.symbol("x");
612    /// let expr = (&x + 1).powi(2);
613    /// assert_eq!(format!("{}", expr.expand()), "x^2 + 2*x + 1");
614    /// ```
615    #[must_use = "returns the expanded form; does not modify in place"]
616    pub fn expand(&self) -> Expr<S> {
617        let _span = debug_span!("expand", expr = ?self.id).entered();
618        let id = self.inner.write().arena.expand_expr(self.id);
619        self.wrap(id)
620    }
621
622    /// Like [`simplify`](Ex::simplify), but with configurable options.
623    ///
624    /// Use [`SimplifyOpts`] to control the fixpoint iteration count.
625    /// This always runs the numeric simplification engine, whatever the
626    /// sort of `self`; for [`BoolEx`] and [`SetEx`] prefer their own
627    /// `simplify()`, which additionally applies boolean / set algebra.
628    ///
629    /// # Examples
630    ///
631    /// ```
632    /// use symplex::prelude::*;
633    ///
634    /// let ctx = Context::new();
635    /// let x = ctx.symbol("x");
636    /// let expr = &x.sin().powi(2) + &x.cos().powi(2);
637    ///
638    /// // Single-pass simplification (no fixpoint iteration)
639    /// let result = expr.simplify_with(&SimplifyOpts::single_pass());
640    /// assert_eq!(format!("{}", result), "1");
641    /// ```
642    #[must_use = "returns the simplified form; does not modify in place"]
643    pub fn simplify_with(&self, opts: &SimplifyOpts) -> Expr<S> {
644        let _span = debug_span!("simplify_with", expr = ?self.id).entered();
645        let result = {
646            let mut inner = self.inner.write();
647            crate::simplify::simplify_engine::unified_simplify(&mut inner.arena, self.id, opts)
648        };
649        self.wrap(result.expr)
650    }
651
652    // ── Serialization ──────────────────────────────────────────────
653
654    /// Convert this expression to a standalone serializable [`ExprTree`](crate::output::tree::ExprTree).
655    ///
656    /// The tree can be serialized to JSON (or any serde format) and
657    /// deserialized back via [`Context::from_tree()`](crate::api::context::Context::from_tree).
658    ///
659    /// # Examples
660    ///
661    /// ```
662    /// use symplex::prelude::*;
663    ///
664    /// let ctx = Context::new();
665    /// let x = ctx.symbol("x");
666    /// let tree = x.powi(2).to_tree();
667    /// let json = serde_json::to_string(&tree).unwrap();
668    /// assert!(json.contains("Pow"));
669    /// ```
670    #[must_use = "returns a serializable tree; does not modify in place"]
671    pub fn to_tree(&self) -> crate::output::tree::ExprTree {
672        let inner = self.inner.read();
673        crate::output::tree::expr_to_tree(&inner.arena, self.id)
674    }
675
676    /// Serialize this expression to a JSON string.
677    ///
678    /// This is a convenience shorthand for
679    /// `serde_json::to_string(&expr.to_tree())`.
680    ///
681    /// # Examples
682    ///
683    /// ```
684    /// use symplex::prelude::*;
685    ///
686    /// let ctx = Context::new();
687    /// let x = ctx.symbol("x");
688    /// let json = x.powi(2).to_json().unwrap();
689    /// assert!(json.contains("\"type\":\"Pow\""));
690    /// ```
691    pub fn to_json(&self) -> Result<String, SymplexError> {
692        serde_json::to_string(&self.to_tree()).map_err(|e| SymplexError::ComputationFailed {
693            operation: "to_json",
694            reason: e.to_string(),
695        })
696    }
697
698    /// Serialize this expression to a pretty-printed JSON string.
699    pub fn to_json_pretty(&self) -> Result<String, SymplexError> {
700        serde_json::to_string_pretty(&self.to_tree()).map_err(|e| SymplexError::ComputationFailed {
701            operation: "to_json_pretty",
702            reason: e.to_string(),
703        })
704    }
705
706    /// Apply a transformation repeatedly until the expression stops changing,
707    /// or `max_iterations` is reached.
708    ///
709    /// Returns the final expression and the number of iterations performed.
710    /// Useful for building custom simplification pipelines.
711    ///
712    /// # Examples
713    ///
714    /// ```
715    /// use symplex::prelude::*;
716    ///
717    /// let ctx = Context::new();
718    /// let x = ctx.symbol("x");
719    /// let expr = (&x + 1).powi(2);
720    /// let (result, iters) = expr.apply_until_stable(10, |e| e.expand());
721    /// assert_eq!(format!("{result}"), "x^2 + 2*x + 1");
722    /// assert_eq!(iters, 1); // stabilized after 1 iteration
723    /// ```
724    #[must_use = "returns the stabilized expression and iteration count"]
725    pub fn apply_until_stable<F>(&self, max_iterations: usize, f: F) -> (Expr<S>, usize)
726    where
727        F: Fn(&Expr<S>) -> Expr<S>,
728    {
729        let mut current = self.clone();
730        for i in 0..max_iterations {
731            let next = f(&current);
732            if next.raw_id() == current.raw_id() && next.ctx_id == current.ctx_id {
733                return (current, i);
734            }
735            current = next;
736        }
737        (current, max_iterations)
738    }
739}
740
741// ═══════════════════════════════════════════════════════════════════════════
742// impl Expr<Numeric> — evaluation / simplification / structural search
743//
744// These used to live on the generic `impl<S: Sort>` block.  They are now
745// per-sort so that `BoolEx` and `SetEx` can provide their own `eval`,
746// `simplify` and `contains` with boolean / set semantics (see
747// `expr_sets_ext.rs`).
748// ═══════════════════════════════════════════════════════════════════════════
749
750impl Expr<Numeric> {
751    /// Returns `true` if `needle` appears as a sub-expression of `self`.
752    ///
753    /// This is a structural check — it walks the expression DAG and
754    /// returns `true` if any node has the same `ExprId` as `needle`.
755    ///
756    /// For set membership use [`SetEx::contains`] / [`Ex::is_in`].
757    #[must_use]
758    pub fn contains(&self, needle: &Ex) -> bool {
759        let needle_id = self.checked_id(needle);
760        let inner = self.inner.read();
761        crate::base::walk::contains(&inner.arena, self.id, needle_id)
762    }
763
764    /// Exact evaluation of known special values.
765    ///
766    /// Replaces function applications with their exact values when the
767    /// arguments are known constants:
768    ///
769    /// - `sin(0)` → `0`, `sin(π)` → `0`, `sin(π/2)` → `1`
770    /// - `cos(0)` → `1`, `cos(π)` → `-1`
771    /// - `exp(0)` → `1`, `ln(1)` → `0`
772    /// - `sqrt(4)` → `2`, `abs(-3)` → `3`
773    ///
774    /// Only evaluates when the result is a simpler atom.
775    /// Does NOT evaluate `cos(π/4)` → `√2/2`.
776    ///
777    /// # Examples
778    ///
779    /// ```
780    /// use symplex::prelude::*;
781    ///
782    /// let ctx = Context::new();
783    /// let expr = ctx.pi().cos();
784    /// assert_eq!(format!("{}", expr.eval()), "-1");
785    /// ```
786    #[must_use = "returns the evaluated form; does not modify in place"]
787    pub fn eval(&self) -> Ex {
788        let _span = debug_span!("eval", expr = ?self.id).entered();
789        let id = self.inner.write().arena.eval_expr(self.id);
790        self.wrap(id)
791    }
792
793    /// Simplification (identity application, trig identities, etc.).
794    ///
795    /// Simplify the expression using all available strategies.
796    ///
797    /// Tries 12+ strategies (eval, expand, factor, trig, log, cancel,
798    /// power, radical, assumption-aware refinement, …), picks the
799    /// simplest result, then iterates to a fixpoint (up to 10 passes)
800    /// until the expression stops getting simpler.
801    ///
802    /// This is the "just make this simpler" function.  For finer control,
803    /// use [`simplify_with`](Self::simplify_with) or the domain-specific
804    /// methods ([`simplify_trig`](Ex::simplify_trig),
805    /// [`simplify_powers`](Ex::simplify_powers), etc.).
806    ///
807    /// # Examples
808    ///
809    /// ```
810    /// use symplex::prelude::*;
811    ///
812    /// let ctx = Context::new();
813    /// let x = ctx.symbol("x");
814    ///
815    /// // Trig identity
816    /// let expr = &x.sin().powi(2) + &x.cos().powi(2);
817    /// assert_eq!(format!("{}", expr.simplify()), "1");
818    ///
819    /// // Polynomial cancellation
820    /// let expr = &(&x + 1).powi(2) - &x.powi(2) - &x * 2;
821    /// assert_eq!(format!("{}", expr.simplify()), "1");
822    /// ```
823    #[must_use = "returns the simplified form; does not modify in place"]
824    pub fn simplify(&self) -> Ex {
825        let _span = debug_span!("simplify", expr = ?self.id).entered();
826        let result = {
827            let mut inner = self.inner.write();
828            crate::simplify::simplify_engine::unified_simplify(
829                &mut inner.arena,
830                self.id,
831                &crate::simplify::simplify_engine::SimplifyOpts::default(),
832            )
833        };
834        self.wrap(result.expr)
835    }
836}
837
838// ═══════════════════════════════════════════════════════════════════════════
839// impl Expr<Boolean> — Boolean-specific methods
840//
841// Boolean algebra (`simplify`, `eval`, CNF/DNF, satisfiability, …) lives
842// in `expr_sets_ext.rs`; the connectives and escape hatches are here.
843// ═══════════════════════════════════════════════════════════════════════════
844
845impl Expr<Boolean> {
846    /// Returns `true` if `needle` appears as a sub-expression of `self`.
847    ///
848    /// This is a structural check — it walks the expression DAG and
849    /// returns `true` if any node has the same `ExprId` as `needle`.
850    #[must_use]
851    pub fn contains(&self, needle: &Ex) -> bool {
852        let needle_id = self.checked_id(needle);
853        let inner = self.inner.read();
854        crate::base::walk::contains(&inner.arena, self.id, needle_id)
855    }
856
857    /// Logical conjunction: `self & other`.
858    #[must_use = "returns a new expression; does not modify in place"]
859    pub fn and(&self, other: &BoolEx) -> BoolEx {
860        let other_id = self.checked_id(other);
861        let id = self.inner.write().arena.and(&[self.id, other_id]);
862        self.wrap(id)
863    }
864
865    /// Logical disjunction: `self | other`.
866    #[must_use = "returns a new expression; does not modify in place"]
867    pub fn or(&self, other: &BoolEx) -> BoolEx {
868        let other_id = self.checked_id(other);
869        let id = self.inner.write().arena.or(&[self.id, other_id]);
870        self.wrap(id)
871    }
872
873    /// Logical negation: `!self`.
874    #[must_use = "returns a new expression; does not modify in place"]
875    pub fn not(&self) -> BoolEx {
876        let id = self.inner.write().arena.not(self.id);
877        self.wrap(id)
878    }
879
880    /// Exclusive or: `self ⊕ other = (self ∧ ¬other) ∨ (¬self ∧ other)`.
881    #[must_use]
882    pub fn xor(&self, other: &BoolEx) -> BoolEx {
883        self.and(&other.not()).or(&self.not().and(other))
884    }
885
886    /// Logical implication: `self → other = ¬self ∨ other`.
887    #[must_use]
888    pub fn implies(&self, other: &BoolEx) -> BoolEx {
889        self.not().or(other)
890    }
891
892    /// Logical biconditional: `self ↔ other = (self → other) ∧ (other → self)`.
893    #[must_use]
894    pub fn equivalent(&self, other: &BoolEx) -> BoolEx {
895        self.implies(other).and(&other.implies(self))
896    }
897
898    /// NAND gate: `¬(self ∧ other)`.
899    #[must_use]
900    pub fn nand(&self, other: &BoolEx) -> BoolEx {
901        self.and(other).not()
902    }
903
904    /// NOR gate: `¬(self ∨ other)`.
905    #[must_use]
906    pub fn nor(&self, other: &BoolEx) -> BoolEx {
907        self.or(other).not()
908    }
909
910    /// If-then-else: `if self then a else b` = `(self ∧ a) ∨ (¬self ∧ b)`.
911    #[must_use]
912    pub fn ite(&self, then_: &BoolEx, else_: &BoolEx) -> BoolEx {
913        let _ = self.checked_id(then_);
914        let _ = self.checked_id(else_);
915        self.and(then_).or(&self.not().and(else_))
916    }
917
918    /// Convert to untyped numeric expression (escape hatch).
919    pub fn into_ex(self) -> Ex {
920        Ex {
921            ctx_id: self.ctx_id,
922            inner: self.inner,
923            id: self.id,
924            _sort: PhantomData,
925        }
926    }
927
928    /// Borrow as untyped numeric expression.
929    pub fn as_ex(&self) -> Ex {
930        Ex {
931            ctx_id: self.ctx_id,
932            inner: Arc::clone(&self.inner),
933            id: self.id,
934            _sort: PhantomData,
935        }
936    }
937}
938
939// ═══════════════════════════════════════════════════════════════════════════
940// impl Expr<SetValued> — Set-specific methods
941//
942// The three constructors below are *lazy* (structural, canonicalised
943// only — principle 1: construction is cheap, evaluation is explicit).
944// Set algebra proper (`simplify`, `difference`, `contains`, `is_subset`,
945// `inf`/`sup`/`measure`, topology, …) lives in `expr_sets_ext.rs`.
946// ═══════════════════════════════════════════════════════════════════════════
947
948impl Expr<SetValued> {
949    /// Union: `self ∪ other` (structural; call [`simplify`](SetEx::simplify)
950    /// to merge overlapping intervals).
951    ///
952    /// # Examples
953    ///
954    /// ```
955    /// use symplex::prelude::*;
956    ///
957    /// let ctx = Context::new();
958    /// let a = ctx.interval(&ctx.int(0), &ctx.int(1), false, false);
959    /// let b = ctx.interval(&ctx.int(2), &ctx.int(3), false, false);
960    /// let u = a.union(&b);
961    /// let s = format!("{u}");
962    /// assert!(!s.is_empty(), "union display: {s}");
963    /// ```
964    #[must_use = "returns a new expression; does not modify in place"]
965    pub fn union(&self, other: &SetEx) -> SetEx {
966        let other_id = self.checked_id(other);
967        let id = self.inner.write().arena.set_union(&[self.id, other_id]);
968        self.wrap(id)
969    }
970
971    /// Intersection: `self ∩ other` (structural; call
972    /// [`simplify`](SetEx::simplify) to evaluate).
973    ///
974    /// # Examples
975    ///
976    /// ```
977    /// use symplex::prelude::*;
978    ///
979    /// let ctx = Context::new();
980    /// let a = ctx.interval(&ctx.int(0), &ctx.int(1), false, false);
981    /// let e = ctx.empty_set();
982    /// let result = a.intersection(&e);
983    /// assert_eq!(format!("{result}"), "EmptySet");
984    /// ```
985    #[must_use = "returns a new expression; does not modify in place"]
986    pub fn intersection(&self, other: &SetEx) -> SetEx {
987        let id = self
988            .inner
989            .write()
990            .arena
991            .set_intersection(&[self.id, self.checked_id(other)]);
992        self.wrap(id)
993    }
994
995    /// Relative complement: `self \ other` (structural).
996    ///
997    /// This is the lazy constructor; [`difference`](SetEx::difference)
998    /// returns the evaluated normal form and
999    /// [`absolute_complement`](SetEx::absolute_complement) computes `ℝ \ self`.
1000    ///
1001    /// # Examples
1002    ///
1003    /// ```
1004    /// use symplex::prelude::*;
1005    ///
1006    /// let ctx = Context::new();
1007    /// let a = ctx.interval(&ctx.int(0), &ctx.int(3), false, false);
1008    /// let b = ctx.interval(&ctx.int(1), &ctx.int(2), true, true);
1009    /// let lazy = a.complement(&b);
1010    /// assert_eq!(format!("{lazy}"), "[0, 3] \\ (1, 2)");
1011    /// assert_eq!(format!("{}", lazy.simplify()), "[0, 1] ∪ [2, 3]");
1012    /// ```
1013    #[must_use = "returns a new expression; does not modify in place"]
1014    pub fn complement(&self, other: &SetEx) -> SetEx {
1015        let other_id = self.checked_id(other);
1016        let id = self.inner.write().arena.set_complement(self.id, other_id);
1017        self.wrap(id)
1018    }
1019
1020    /// Convert to untyped numeric expression (escape hatch).
1021    ///
1022    /// This allows set-valued expressions to be embedded in contexts
1023    /// that expect `Ex`. The underlying arena node is unchanged.
1024    pub fn into_ex(self) -> Ex {
1025        Ex {
1026            ctx_id: self.ctx_id,
1027            inner: self.inner,
1028            id: self.id,
1029            _sort: PhantomData,
1030        }
1031    }
1032
1033    /// Borrow as untyped numeric expression.
1034    pub fn as_ex(&self) -> Ex {
1035        Ex {
1036            ctx_id: self.ctx_id,
1037            inner: Arc::clone(&self.inner),
1038            id: self.id,
1039            _sort: PhantomData,
1040        }
1041    }
1042}