Skip to main content

ocas_eval/
domain.rs

1//! Numeric evaluation domain trait.
2//!
3//! The [`EvaluationDomain`] trait abstracts over numeric types that can be
4//! used for expression evaluation. It provides arithmetic operations and
5//! a table of built-in mathematical functions (sin, cos, exp, etc.).
6//!
7//! # Implementations
8//!
9//! - `f64` — standard double-precision floating point (always available)
10//! - `ocas_domain::Integer` — arbitrary-precision integers
11//! - `ocas_domain::Rational` — arbitrary-precision rationals
12//! - `ocas_domain::RealBall` — rigorous real interval arithmetic
13
14use crate::error::{EvaluationError, Result};
15
16/// Trait for types that can serve as evaluation domains.
17///
18/// Unlike the algebraic [`Domain`](ocas_domain::Domain) trait which is
19/// object-safe and uses `&self` receivers, `EvaluationDomain` uses static
20/// methods and `&self`/`&other` for value operations. This makes it
21/// compatible with `f64` and other `Copy` types.
22///
23/// # Example
24///
25/// ```
26/// use ocas_eval::EvaluationDomain;
27///
28/// let x = f64::from_f64(3.0);
29/// let y = f64::from_f64(2.0);
30/// assert_eq!(x.add_ref(&y), 5.0);
31/// assert_eq!(f64::resolve_builtin("sin", &std::f64::consts::FRAC_PI_2).unwrap(), 1.0);
32/// ```
33pub trait EvaluationDomain: Sized + Clone + 'static {
34    /// Create a value from an `f64`.
35    fn from_f64(value: f64) -> Self;
36
37    /// The additive identity (0).
38    fn zero() -> Self;
39
40    /// The multiplicative identity (1).
41    fn one() -> Self;
42
43    /// `self + other`
44    fn add_ref(&self, other: &Self) -> Self;
45
46    /// `self - other`
47    fn sub_ref(&self, other: &Self) -> Self;
48
49    /// `self * other`
50    fn mul_ref(&self, other: &Self) -> Self;
51
52    /// `self / other`, or [`EvaluationError::DivisionByZero`] if `other` is zero.
53    fn div_ref(&self, other: &Self) -> Result<Self>;
54
55    /// `-self`
56    fn neg_ref(&self) -> Self;
57
58    /// `self^exp` for integer exponents. Returns 1 when exp == 0.
59    fn powi_ref(&self, exp: i64) -> Self;
60
61    /// Resolve a built-in mathematical function.
62    ///
63    /// Accepts both lowercase (`sin`) and capitalized (`Sin`) names.
64    /// The following functions are supported:
65    ///
66    /// | Function | Description |
67    /// |---|---|
68    /// | `sin` / `Sin` | Sine |
69    /// | `cos` / `Cos` | Cosine |
70    /// | `tan` / `Tan` | Tangent |
71    /// | `sec` / `Sec` | Secant |
72    /// | `csc` / `Csc` | Cosecant |
73    /// | `cot` / `Cot` | Cotangent |
74    /// | `exp` / `Exp` | Exponential (eˣ) |
75    /// | `log` / `Log` | Natural logarithm |
76    /// | `sqrt` / `Sqrt` | Square root |
77    /// | `abs` / `Abs` | Absolute value |
78    fn resolve_builtin(name: &str, arg: &Self) -> Result<Self>;
79}
80
81// ---------------------------------------------------------------------------
82// f64 implementation
83// ---------------------------------------------------------------------------
84
85impl EvaluationDomain for f64 {
86    #[inline]
87    fn from_f64(value: f64) -> Self {
88        value
89    }
90
91    #[inline]
92    fn zero() -> Self {
93        0.0
94    }
95
96    #[inline]
97    fn one() -> Self {
98        1.0
99    }
100
101    #[inline]
102    fn add_ref(&self, other: &Self) -> Self {
103        self + other
104    }
105
106    #[inline]
107    fn sub_ref(&self, other: &Self) -> Self {
108        self - other
109    }
110
111    #[inline]
112    fn mul_ref(&self, other: &Self) -> Self {
113        self * other
114    }
115
116    #[inline]
117    fn div_ref(&self, other: &Self) -> Result<Self> {
118        if *other == 0.0 {
119            Err(EvaluationError::DivisionByZero)
120        } else {
121            Ok(self / other)
122        }
123    }
124
125    #[inline]
126    fn neg_ref(&self) -> Self {
127        -self
128    }
129
130    #[inline]
131    fn powi_ref(&self, exp: i64) -> Self {
132        self.powi(exp as i32)
133    }
134
135    fn resolve_builtin(name: &str, arg: &Self) -> Result<Self> {
136        match name.to_lowercase().as_str() {
137            "sin" => Ok(arg.sin()),
138            "cos" => Ok(arg.cos()),
139            "tan" => Ok(arg.tan()),
140            "sec" => Ok(1.0 / arg.cos()),
141            "csc" => Ok(1.0 / arg.sin()),
142            "cot" => Ok(1.0 / arg.tan()),
143            "exp" => Ok(arg.exp()),
144            "log" => {
145                if *arg <= 0.0 {
146                    Err(EvaluationError::UnsupportedOperation {
147                        message: "log of non-positive number".into(),
148                    })
149                } else {
150                    Ok(arg.ln())
151                }
152            }
153            "sqrt" => {
154                if *arg < 0.0 {
155                    Err(EvaluationError::UnsupportedOperation {
156                        message: "sqrt of negative number".into(),
157                    })
158                } else {
159                    Ok(arg.sqrt())
160                }
161            }
162            "abs" => Ok(arg.abs()),
163            _ => Err(EvaluationError::FunctionNotFound {
164                name: name.to_string(),
165            }),
166        }
167    }
168}
169
170// ---------------------------------------------------------------------------
171// PowfExtension
172// ---------------------------------------------------------------------------
173
174/// Extension to [`EvaluationDomain`] for floating-point exponentiation.
175///
176/// This is split from the main trait because integer domains cannot
177/// meaningfully compute `a^b` for non-integer `b`.
178pub trait PowfExtension: EvaluationDomain {
179    /// `self^exp` for floating-point exponents.
180    fn powf_ref(&self, exp: &Self) -> Result<Self>;
181}
182
183impl PowfExtension for f64 {
184    fn powf_ref(&self, exp: &Self) -> Result<Self> {
185        Ok(self.powf(*exp))
186    }
187}
188
189// =========================================================================
190// DoubleF64 implementation
191// =========================================================================
192
193use ocas_domain::DoubleF64;
194
195impl EvaluationDomain for DoubleF64 {
196    #[inline]
197    fn from_f64(value: f64) -> Self {
198        DoubleF64::from_f64(value)
199    }
200
201    #[inline]
202    fn zero() -> Self {
203        DoubleF64::ZERO
204    }
205
206    #[inline]
207    fn one() -> Self {
208        DoubleF64::ONE
209    }
210
211    #[inline]
212    fn add_ref(&self, other: &Self) -> Self {
213        *self + *other
214    }
215
216    #[inline]
217    fn sub_ref(&self, other: &Self) -> Self {
218        *self - *other
219    }
220
221    #[inline]
222    fn mul_ref(&self, other: &Self) -> Self {
223        *self * *other
224    }
225
226    #[inline]
227    fn div_ref(&self, other: &Self) -> Result<Self> {
228        if other.hi == 0.0 && other.lo == 0.0 {
229            Err(EvaluationError::DivisionByZero)
230        } else {
231            Ok(*self / *other)
232        }
233    }
234
235    #[inline]
236    fn neg_ref(&self) -> Self {
237        -*self
238    }
239
240    #[inline]
241    fn powi_ref(&self, exp: i64) -> Self {
242        self.powi(exp)
243    }
244
245    fn resolve_builtin(name: &str, arg: &Self) -> Result<Self> {
246        match name.to_lowercase().as_str() {
247            "sin" => Ok(arg.sin()),
248            "cos" => Ok(arg.cos()),
249            "tan" => Ok(arg.tan()),
250            "sec" => Ok(DoubleF64::ONE / arg.cos()),
251            "csc" => Ok(DoubleF64::ONE / arg.sin()),
252            "cot" => Ok(DoubleF64::ONE / arg.tan()),
253            "exp" => Ok(arg.exp()),
254            "log" => {
255                if arg.hi <= 0.0 {
256                    Err(EvaluationError::UnsupportedOperation {
257                        message: "log of non-positive number".into(),
258                    })
259                } else {
260                    Ok(arg.ln())
261                }
262            }
263            "sqrt" => {
264                if arg.hi < 0.0 {
265                    Err(EvaluationError::UnsupportedOperation {
266                        message: "sqrt of negative number".into(),
267                    })
268                } else {
269                    Ok(arg.sqrt())
270                }
271            }
272            "abs" => Ok(arg.dabs()),
273            _ => Err(EvaluationError::FunctionNotFound {
274                name: name.to_string(),
275            }),
276        }
277    }
278}
279
280impl PowfExtension for DoubleF64 {
281    fn powf_ref(&self, exp: &Self) -> Result<Self> {
282        // a^b = exp(b * ln(a))
283        if self.hi <= 0.0 {
284            return Err(EvaluationError::UnsupportedOperation {
285                message: "powf with non-positive base".into(),
286            });
287        }
288        Ok(exp.mul(self.ln()).exp())
289    }
290}
291
292// ---------------------------------------------------------------------------
293// Tests
294// ---------------------------------------------------------------------------
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn f64_arithmetic() {
302        assert_eq!(f64::zero(), 0.0);
303        assert_eq!(f64::one(), 1.0);
304        assert_eq!(3.0f64.add_ref(&2.0), 5.0);
305        assert_eq!(3.0f64.sub_ref(&2.0), 1.0);
306        assert_eq!(3.0f64.mul_ref(&2.0), 6.0);
307        assert_eq!(6.0f64.div_ref(&2.0).unwrap(), 3.0);
308        assert!(6.0f64.div_ref(&0.0).is_err());
309        assert_eq!(3.0f64.neg_ref(), -3.0);
310        assert_eq!(2.0f64.powi_ref(3), 8.0);
311        assert_eq!(2.0f64.powi_ref(0), 1.0);
312    }
313
314    #[test]
315    fn f64_builtin_sin_lowercase() {
316        let result = f64::resolve_builtin("sin", &std::f64::consts::FRAC_PI_2).unwrap();
317        assert!((result - 1.0).abs() < 1e-10);
318    }
319
320    #[test]
321    fn f64_builtin_sin_capitalized() {
322        let result = f64::resolve_builtin("Sin", &std::f64::consts::FRAC_PI_2).unwrap();
323        assert!((result - 1.0).abs() < 1e-10);
324    }
325
326    #[test]
327    fn f64_builtin_cos() {
328        let result = f64::resolve_builtin("cos", &std::f64::consts::PI).unwrap();
329        assert!((result + 1.0).abs() < 1e-10);
330    }
331
332    #[test]
333    fn f64_builtin_exp() {
334        let result = f64::resolve_builtin("exp", &1.0).unwrap();
335        assert!((result - std::f64::consts::E).abs() < 1e-10);
336    }
337
338    #[test]
339    fn f64_builtin_log() {
340        let result = f64::resolve_builtin("log", &std::f64::consts::E).unwrap();
341        assert!((result - 1.0).abs() < 1e-10);
342    }
343
344    #[test]
345    fn f64_builtin_log_negative() {
346        assert!(f64::resolve_builtin("log", &(-1.0)).is_err());
347    }
348
349    #[test]
350    fn f64_builtin_sqrt() {
351        let result = f64::resolve_builtin("sqrt", &4.0).unwrap();
352        assert!((result - 2.0).abs() < 1e-10);
353    }
354
355    #[test]
356    fn f64_builtin_sqrt_negative() {
357        assert!(f64::resolve_builtin("sqrt", &(-1.0)).is_err());
358    }
359
360    #[test]
361    fn f64_builtin_abs() {
362        assert_eq!(f64::resolve_builtin("abs", &(-3.0)).unwrap(), 3.0);
363        assert_eq!(f64::resolve_builtin("abs", &3.0).unwrap(), 3.0);
364    }
365
366    #[test]
367    fn f64_builtin_tan() {
368        let result = f64::resolve_builtin("tan", &0.0).unwrap();
369        assert!((result - 0.0).abs() < 1e-10);
370    }
371
372    #[test]
373    fn f64_builtin_unknown() {
374        assert!(f64::resolve_builtin("unknown_fn", &0.0).is_err());
375    }
376
377    #[test]
378    fn f64_from_f64() {
379        assert_eq!(f64::from_f64(42.0), 42.0);
380    }
381}