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(¤t);
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}