Skip to main content

ocas_py/
expression.rs

1//! Python `Expression` — a self-contained symbolic expression.
2//!
3//! Each [`Expression`] owns a private leaked `Arena` + `AtomArena<'static>`,
4//! recovered on `Drop`. This mirrors the C API design and avoids cross-
5//! reference lifetime entanglement between Python objects.
6
7use ocas_atom::{Atom, AtomArena, Symbol, normalize::normalize};
8use ocas_calc::{diff, integrate, integrate_heuristic, substitute, taylor};
9use ocas_core::arena::Arena;
10use ocas_parse::parse;
11use ocas_rewrite::rules::default_rules;
12use ocas_rewrite::simplify::simplify;
13use pyo3::exceptions::PyValueError;
14use pyo3::prelude::*;
15
16/// Internal storage behind an [`Expression`]: a leaked arena pair recovered
17/// on drop.
18struct ExprInner {
19    arena_ptr: *mut Arena,
20    ctx_ptr: *mut AtomArena<'static>,
21    atom: Atom<'static>,
22}
23
24// SAFETY: the two heap allocations are not tied to any thread. The atom
25// borrows them but they live until Drop.
26unsafe impl Send for ExprInner {}
27// SAFETY: pyo3 `#[pyclass]` (without `unordered`) requires `Send + Sync`.
28// All `&self` method invocations are serialized by the GIL, so the
29// `RefCell` inside `AtomArena` is never accessed concurrently.
30// IMPORTANT: do not call `Python::allow_threads` with closures that access
31// `ExprInner` — that would release the GIL and break this invariant.
32unsafe impl Sync for ExprInner {}
33
34impl Drop for ExprInner {
35    fn drop(&mut self) {
36        // SAFETY: both pointers came from `Box::into_raw`. Drop `ctx_ptr`
37        // first because it borrows `arena_ptr`.
38        unsafe {
39            let _ = Box::from_raw(self.ctx_ptr);
40            let _ = Box::from_raw(self.arena_ptr);
41        }
42    }
43}
44
45/// RAII guard that frees the leaked arena pair unless explicitly disarmed.
46/// See [`ExprInner::build`] for usage.
47struct ArenaGuard {
48    arena_ptr: *mut Arena,
49    ctx_ptr: *mut AtomArena<'static>,
50    armed: bool,
51}
52
53impl ArenaGuard {
54    fn new(arena_ptr: *mut Arena, ctx_ptr: *mut AtomArena<'static>) -> Self {
55        ArenaGuard {
56            arena_ptr,
57            ctx_ptr,
58            armed: true,
59        }
60    }
61
62    fn disarm(&mut self) {
63        self.armed = false;
64    }
65}
66
67impl Drop for ArenaGuard {
68    fn drop(&mut self) {
69        if self.armed {
70            // SAFETY: both pointers came from `Box::into_raw`.
71            unsafe {
72                let _ = Box::from_raw(self.ctx_ptr);
73                let _ = Box::from_raw(self.arena_ptr);
74            }
75        }
76    }
77}
78
79impl ExprInner {
80    /// Borrow the atom arena as `&'static AtomArena<'static>`.
81    fn ctx(&self) -> &'static AtomArena<'static> {
82        // SAFETY: valid for as long as `ExprInner` is alive.
83        unsafe { &*self.ctx_ptr }
84    }
85
86    /// Allocate a fresh arena pair.
87    fn new_pair() -> (*mut Arena, *mut AtomArena<'static>) {
88        let arena_box: Box<Arena> = Box::new(Arena::new());
89        let arena_ptr = Box::into_raw(arena_box);
90        // SAFETY: `arena_ptr` outlives `ExprInner`; recovered in Drop.
91        let arena_ref: &'static Arena = unsafe { &*arena_ptr };
92        let ctx = AtomArena::new(arena_ref);
93        let ctx_ptr = Box::into_raw(Box::new(ctx));
94        (arena_ptr, ctx_ptr)
95    }
96
97    /// Build from a closure that receives `&'static AtomArena<'static>`.
98    fn build<F>(f: F) -> PyResult<Box<Self>>
99    where
100        F: FnOnce(&'static AtomArena<'static>) -> Result<Atom<'static>, String>,
101    {
102        let (arena_ptr, ctx_ptr) = Self::new_pair();
103        let mut guard = ArenaGuard::new(arena_ptr, ctx_ptr);
104        let ctx = unsafe { &*ctx_ptr };
105        // If `f` or `normalize` panics, `guard` is dropped and frees the
106        // arenas. On success we disarm and transfer ownership to ExprInner.
107        let atom = f(ctx).map_err(PyValueError::new_err)?;
108        let normalized = normalize(ctx, atom);
109        guard.disarm();
110        Ok(Box::new(ExprInner {
111            arena_ptr,
112            ctx_ptr,
113            atom: normalized,
114        }))
115    }
116
117    /// Parse a string.
118    fn from_str(input: &str) -> PyResult<Box<Self>> {
119        Self::build(|ctx| match parse(ctx, input) {
120            Ok(a) => Ok(a),
121            Err(e) => Err(format!("parse error: {e}")),
122        })
123    }
124
125    /// Rebuild from the string form of `src`.
126    fn from_string_src(src: String) -> PyResult<Box<Self>> {
127        Self::build(|ctx| match parse(ctx, &src) {
128            Ok(a) => Ok(a),
129            Err(e) => Err(format!("parse error: {e}")),
130        })
131    }
132}
133
134/// A symbolic expression.
135///
136/// Construct from a string:
137///
138/// ```python
139/// from ocas import Expression
140/// e = Expression("x^2 + 2*x + 1")
141/// print(e.diff("x"))
142/// ```
143#[pyclass(name = "Expression")]
144pub struct Expression {
145    inner: Box<ExprInner>,
146}
147
148impl Expression {
149    /// Borrow the expression's arena (crate-internal).
150    pub(crate) fn ctx_ref(&self) -> &'static AtomArena<'static> {
151        self.inner.ctx()
152    }
153
154    /// Borrow the underlying atom (crate-internal).
155    pub(crate) fn atom(&self) -> Atom<'static> {
156        self.inner.atom
157    }
158}
159
160#[pymethods]
161impl Expression {
162    /// Parse a string into an expression.
163    #[new]
164    fn new(input: &str) -> PyResult<Self> {
165        Ok(Expression {
166            inner: ExprInner::from_str(input)?,
167        })
168    }
169
170    fn __str__(&self) -> String {
171        self.inner.atom.to_string()
172    }
173
174    fn __repr__(&self) -> String {
175        format!("Expression({:?})", self.inner.atom.to_string())
176    }
177
178    fn __add__(&self, other: &Expression) -> PyResult<Expression> {
179        let left = self.inner.atom.to_string();
180        let right = other.inner.atom.to_string();
181        let combined = format!("({left}) + ({right})");
182        Ok(Expression {
183            inner: ExprInner::from_string_src(combined)?,
184        })
185    }
186
187    fn __sub__(&self, other: &Expression) -> PyResult<Expression> {
188        let left = self.inner.atom.to_string();
189        let right = other.inner.atom.to_string();
190        let combined = format!("({left}) + (-1)*({right})");
191        Ok(Expression {
192            inner: ExprInner::from_string_src(combined)?,
193        })
194    }
195
196    fn __mul__(&self, other: &Expression) -> PyResult<Expression> {
197        let left = self.inner.atom.to_string();
198        let right = other.inner.atom.to_string();
199        let combined = format!("({left})*({right})");
200        Ok(Expression {
201            inner: ExprInner::from_string_src(combined)?,
202        })
203    }
204
205    fn __pow__(&self, other: &Expression, _modulo: Option<&Expression>) -> PyResult<Expression> {
206        let left = self.inner.atom.to_string();
207        let right = other.inner.atom.to_string();
208        let combined = format!("({left})^({right})");
209        Ok(Expression {
210            inner: ExprInner::from_string_src(combined)?,
211        })
212    }
213
214    fn __neg__(&self) -> PyResult<Expression> {
215        let src = self.inner.atom.to_string();
216        Ok(Expression {
217            inner: ExprInner::from_string_src(format!("(-1)*({src})"))?,
218        })
219    }
220
221    fn __eq__(&self, other: &Expression) -> bool {
222        // Compare normalized string forms.
223        let a = normalize(self.inner.ctx(), self.inner.atom);
224        let b = normalize(other.inner.ctx(), other.inner.atom);
225        a.to_string() == b.to_string()
226    }
227
228    fn __hash__(&self) -> u64 {
229        use std::collections::hash_map::DefaultHasher;
230        use std::hash::{Hash, Hasher};
231        let mut h = DefaultHasher::new();
232        self.inner.atom.to_string().hash(&mut h);
233        h.finish()
234    }
235
236    /// Return a copy of this expression.
237    fn clone(&self) -> PyResult<Expression> {
238        let src = self.inner.atom.to_string();
239        Ok(Expression {
240            inner: ExprInner::from_string_src(src)?,
241        })
242    }
243
244    /// Simplify using the default rule set.
245    fn simplify(&self) -> PyResult<Expression> {
246        let src = self.inner.atom.to_string();
247        ExprInner::build(|ctx| {
248            let a = parse(ctx, &src).map_err(|e| e.to_string())?;
249            let rules = default_rules(ctx, &());
250            Ok(simplify(ctx, a, &rules, 20))
251        })
252        .map(|inner| Expression { inner })
253    }
254
255    /// Differentiate with respect to `var`.
256    fn diff(&self, var: &str) -> PyResult<Expression> {
257        let src = self.inner.atom.to_string();
258        let var_sym = Symbol::new(var);
259        ExprInner::build(|ctx| match parse(ctx, &src) {
260            Ok(a) => Ok(diff(ctx, a, var_sym)),
261            Err(e) => Err(e.to_string()),
262        })
263        .map(|inner| Expression { inner })
264    }
265
266    /// Integrate with respect to `var`.
267    fn integrate(&self, var: &str) -> PyResult<Expression> {
268        let src = self.inner.atom.to_string();
269        let var_sym = Symbol::new(var);
270        ExprInner::build(|ctx| match parse(ctx, &src) {
271            Ok(a) => Ok(integrate(ctx, a, var_sym)),
272            Err(e) => Err(e.to_string()),
273        })
274        .map(|inner| Expression { inner })
275    }
276
277    /// Integrate using heuristic techniques (integration by parts,
278    /// trigonometric substitution, Weierstrass, Euler).
279    ///
280    /// Returns the antiderivative if a heuristic succeeds, or the
281    /// unevaluated ``Integral(expr, var)`` form otherwise.
282    fn integrate_heuristic(&self, var: &str) -> PyResult<Expression> {
283        let src = self.inner.atom.to_string();
284        let var_sym = Symbol::new(var);
285        ExprInner::build(|ctx| match parse(ctx, &src) {
286            Ok(a) => Ok(integrate_heuristic(ctx, a, var_sym)),
287            Err(e) => Err(e.to_string()),
288        })
289        .map(|inner| Expression { inner })
290    }
291
292    /// Compute the Taylor series around `point` up to `order`.
293    fn taylor(&self, var: &str, point: &Expression, order: usize) -> PyResult<Expression> {
294        let expr_src = self.inner.atom.to_string();
295        let point_src = point.inner.atom.to_string();
296        let var_sym = Symbol::new(var);
297        ExprInner::build(|ctx| {
298            let e = parse(ctx, &expr_src).map_err(|e| e.to_string())?;
299            let p = parse(ctx, &point_src).map_err(|e| e.to_string())?;
300            Ok(taylor(ctx, e, var_sym, p, order))
301        })
302        .map(|inner| Expression { inner })
303    }
304
305    /// Substitute every occurrence of `var` with `replacement`.
306    fn substitute(&self, var: &str, replacement: &Expression) -> PyResult<Expression> {
307        let expr_src = self.inner.atom.to_string();
308        let repl_src = replacement.inner.atom.to_string();
309        let var_sym = Symbol::new(var);
310        ExprInner::build(|ctx| {
311            let e = parse(ctx, &expr_src).map_err(|e| e.to_string())?;
312            let r = parse(ctx, &repl_src).map_err(|e| e.to_string())?;
313            Ok(substitute(ctx, e, var_sym, r))
314        })
315        .map(|inner| Expression { inner })
316    }
317}