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