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