Skip to main content

ries_rs/
eval.rs

1//! Expression evaluation with automatic differentiation
2//!
3//! Evaluates postfix expressions and computes derivatives using forward-mode AD.
4//!
5//! # Performance
6//!
7//! For hot loops (generation, Newton-Raphson), use `evaluate_with_workspace()` with
8//! a reusable `EvalWorkspace` to avoid heap allocations on every call.
9
10use crate::expr::Expression;
11use crate::profile::UserConstant;
12use crate::symbol::{NumType, Seft, Symbol};
13use crate::udf::{UdfOp, UserFunction};
14
15/// Result of evaluating an expression
16#[derive(Debug, Clone, Copy)]
17pub struct EvalResult {
18    /// The computed value
19    pub value: f64,
20    /// Derivative with respect to x
21    pub derivative: f64,
22    /// Numeric type of the result
23    pub num_type: NumType,
24}
25
26/// Evaluation error types
27///
28/// These errors indicate what went wrong during expression evaluation.
29/// For more detailed context, use the error message methods.
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
31pub enum EvalError {
32    /// Stack underflow during evaluation
33    #[error("Stack underflow: not enough operands on stack")]
34    StackUnderflow,
35    /// User constant slot referenced by the expression is not configured
36    #[error("Missing user constant: slot u{0} is not configured")]
37    MissingUserConstant(usize),
38    /// Division by zero
39    #[error("Division by zero: divisor was zero or near-zero")]
40    DivisionByZero,
41    /// Logarithm of non-positive number
42    #[error("Logarithm domain error: argument was non-positive")]
43    LogDomain,
44    /// Square root of negative number
45    #[error("Square root domain error: argument was negative")]
46    SqrtDomain,
47    /// Overflow or NaN result
48    #[error("Overflow: result is infinite or NaN")]
49    Overflow,
50    /// Invalid expression
51    #[error("Invalid expression: malformed or incomplete")]
52    Invalid,
53    /// Error with position context
54    #[error("{err} at position {pos}")]
55    WithPosition {
56        #[source]
57        err: Box<EvalError>,
58        pos: usize,
59    },
60    /// Error with value context
61    #[error("{err} (value: {val})")]
62    WithValue {
63        #[source]
64        err: Box<EvalError>,
65        val: ordered_float::OrderedFloat<f64>,
66    },
67    /// Error with expression context
68    #[error("{err} in expression '{expr}'")]
69    WithExpression {
70        #[source]
71        err: Box<EvalError>,
72        expr: String,
73    },
74}
75
76impl EvalError {
77    /// Create a detailed error message with context (backward compatibility)
78    pub fn with_context(self, position: Option<usize>, value: Option<f64>) -> Self {
79        let mut err = self;
80        if let Some(pos) = position {
81            err = EvalError::WithPosition {
82                err: Box::new(err),
83                pos,
84            };
85        }
86        if let Some(val) = value {
87            err = EvalError::WithValue {
88                err: Box::new(err),
89                val: ordered_float::OrderedFloat(val),
90            };
91        }
92        err
93    }
94
95    /// Add expression context
96    pub fn with_expression(self, expr: String) -> Self {
97        EvalError::WithExpression {
98            err: Box::new(self),
99            expr,
100        }
101    }
102}
103
104/// Mathematical constants
105pub mod constants {
106    pub const PI: f64 = std::f64::consts::PI;
107    pub const E: f64 = std::f64::consts::E;
108    pub const PHI: f64 = 1.618_033_988_749_895; // Golden ratio
109    /// Euler-Mascheroni constant γ
110    pub const GAMMA: f64 = 0.577_215_664_901_532_9;
111    /// Plastic constant ρ (root of x³ = x + 1)
112    pub const PLASTIC: f64 = 1.324_717_957_244_746;
113    /// Apéry's constant ζ(3)
114    pub const APERY: f64 = 1.202_056_903_159_594_2;
115    /// Catalan's constant G
116    pub const CATALAN: f64 = 0.915_965_594_177_219;
117}
118
119/// Default trig argument scale used by `sinpi/cospi/tanpi`.
120///
121/// This matches original `sinpi(x) = sin(πx)` semantics.
122pub const DEFAULT_TRIG_ARGUMENT_SCALE: f64 = std::f64::consts::PI;
123
124/// Tolerance for snapping default `sinpi/cospi/tanpi` arguments to exact
125/// integer and half-integer values.
126const TRIG_EXACT_ARGUMENT_TOLERANCE: f64 = 1e-12;
127
128/// Explicit evaluation context for a single run.
129///
130/// This keeps trig scaling and user-defined symbols inside the function
131/// signature instead of relying on process-global evaluator state.
132#[derive(Clone, Copy, Debug)]
133pub struct EvalContext<'a> {
134    /// Argument scale for `sinpi/cospi/tanpi`.
135    pub trig_argument_scale: f64,
136    /// User-defined constants available during evaluation.
137    pub user_constants: &'a [UserConstant],
138    /// User-defined functions available during evaluation.
139    pub user_functions: &'a [UserFunction],
140}
141
142impl Default for EvalContext<'static> {
143    fn default() -> Self {
144        Self {
145            trig_argument_scale: DEFAULT_TRIG_ARGUMENT_SCALE,
146            user_constants: &[],
147            user_functions: &[],
148        }
149    }
150}
151
152impl EvalContext<'static> {
153    /// Create a default context with built-in trig semantics and no user symbols.
154    pub fn new() -> Self {
155        Self::default()
156    }
157}
158
159impl<'a> EvalContext<'a> {
160    /// Create a context from user-defined constants and functions.
161    pub fn from_slices(
162        user_constants: &'a [UserConstant],
163        user_functions: &'a [UserFunction],
164    ) -> Self {
165        Self {
166            trig_argument_scale: DEFAULT_TRIG_ARGUMENT_SCALE,
167            user_constants,
168            user_functions,
169        }
170    }
171
172    /// Override the trig argument scale for this evaluation context.
173    pub fn with_trig_argument_scale(mut self, scale: f64) -> Self {
174        if scale.is_finite() && scale != 0.0 {
175            self.trig_argument_scale = scale;
176        }
177        self
178    }
179}
180
181/// Stack entry for evaluation with derivative tracking
182#[derive(Debug, Clone, Copy)]
183struct StackEntry {
184    val: f64,
185    deriv: f64,
186    num_type: NumType,
187}
188
189impl StackEntry {
190    fn new(val: f64, deriv: f64, num_type: NumType) -> Self {
191        Self {
192            val,
193            deriv,
194            num_type,
195        }
196    }
197
198    fn constant(val: f64, num_type: NumType) -> Self {
199        Self {
200            val,
201            deriv: 0.0,
202            num_type,
203        }
204    }
205}
206
207/// Reusable workspace for expression evaluation.
208///
209/// Using a workspace avoids heap allocations on every `evaluate()` call,
210/// which is critical for performance in hot loops (generation, Newton-Raphson).
211///
212/// # Example
213///
214/// ```no_run
215/// use ries_rs::eval::{EvalWorkspace, evaluate_with_workspace};
216/// use ries_rs::expr::Expression;
217/// let mut workspace = EvalWorkspace::new();
218/// let expressions: Vec<Expression> = vec![];
219/// let x = 1.0_f64;
220/// for expr in &expressions {
221///     let result = evaluate_with_workspace(expr, x, &mut workspace)?;
222///     // workspace is reused, no new allocations
223/// }
224/// # Ok::<(), ries_rs::eval::EvalError>(())
225/// ```
226pub struct EvalWorkspace {
227    stack: Vec<StackEntry>,
228}
229
230impl EvalWorkspace {
231    /// Create a new workspace with pre-allocated capacity.
232    ///
233    /// Capacity of 32 handles most expressions; grows automatically if needed.
234    pub fn new() -> Self {
235        Self {
236            stack: Vec::with_capacity(32),
237        }
238    }
239
240    /// Clear the workspace for reuse (keeps allocated capacity).
241    #[inline]
242    fn clear(&mut self) {
243        self.stack.clear();
244    }
245}
246
247impl Default for EvalWorkspace {
248    fn default() -> Self {
249        Self::new()
250    }
251}
252
253/// Evaluate an expression at a given value of x, using a reusable workspace.
254///
255/// This is the hot-path version that avoids heap allocations.
256/// Use this in loops where `evaluate()` is called many times.
257///
258/// Note: This is a convenience wrapper for the full `evaluate_with_workspace_and_constants_and_functions`
259/// when you don't need user constants or functions. It's provided as a simpler API for common cases.
260#[inline]
261pub fn evaluate_with_workspace(
262    expr: &Expression,
263    x: f64,
264    workspace: &mut EvalWorkspace,
265) -> Result<EvalResult, EvalError> {
266    evaluate_with_workspace_and_context(expr, x, workspace, &EvalContext::new())
267}
268
269/// Evaluate an expression with user constants, using a reusable workspace.
270///
271/// This is the hot-path version that avoids heap allocations.
272/// The `user_constants` slice provides values for `UserConstant0..15` symbols.
273///
274/// Note: This is a convenience wrapper for the full `evaluate_with_workspace_and_constants_and_functions`
275/// when you don't need user functions. It's provided as a simpler API for common cases.
276#[inline]
277pub fn evaluate_with_workspace_and_constants(
278    expr: &Expression,
279    x: f64,
280    workspace: &mut EvalWorkspace,
281    user_constants: &[UserConstant],
282) -> Result<EvalResult, EvalError> {
283    let context = EvalContext::from_slices(user_constants, &[]);
284    evaluate_with_workspace_and_context(expr, x, workspace, &context)
285}
286
287/// Evaluate an expression with user constants and user functions, using a reusable workspace.
288///
289/// This is the full hot-path version that avoids heap allocations.
290/// The `user_constants` slice provides values for `UserConstant0..15` symbols.
291/// The `user_functions` slice provides bodies for `UserFunction0..15` symbols.
292#[inline]
293pub fn evaluate_with_workspace_and_constants_and_functions(
294    expr: &Expression,
295    x: f64,
296    workspace: &mut EvalWorkspace,
297    user_constants: &[UserConstant],
298    user_functions: &[UserFunction],
299) -> Result<EvalResult, EvalError> {
300    let context = EvalContext::from_slices(user_constants, user_functions);
301    evaluate_with_workspace_and_context(expr, x, workspace, &context)
302}
303
304/// Evaluate an expression using an explicit evaluation context and reusable workspace.
305///
306/// This is the preferred hot-path API for library consumers that need explicit
307/// control over trig semantics or user-defined symbols.
308#[inline]
309pub fn evaluate_with_workspace_and_context(
310    expr: &Expression,
311    x: f64,
312    workspace: &mut EvalWorkspace,
313    context: &EvalContext<'_>,
314) -> Result<EvalResult, EvalError> {
315    workspace.clear();
316    let stack = &mut workspace.stack;
317
318    for &sym in expr.symbols() {
319        match sym.seft() {
320            Seft::A => {
321                let entry = eval_constant_with_user(sym, x, context.user_constants)?;
322                stack.push(entry);
323            }
324            Seft::B => {
325                // Check if this is a user function
326                if matches!(
327                    sym,
328                    Symbol::UserFunction0
329                        | Symbol::UserFunction1
330                        | Symbol::UserFunction2
331                        | Symbol::UserFunction3
332                        | Symbol::UserFunction4
333                        | Symbol::UserFunction5
334                        | Symbol::UserFunction6
335                        | Symbol::UserFunction7
336                        | Symbol::UserFunction8
337                        | Symbol::UserFunction9
338                        | Symbol::UserFunction10
339                        | Symbol::UserFunction11
340                        | Symbol::UserFunction12
341                        | Symbol::UserFunction13
342                        | Symbol::UserFunction14
343                        | Symbol::UserFunction15
344                ) {
345                    // Evaluate user function
346                    let a = stack.pop().ok_or(EvalError::StackUnderflow)?;
347                    let result = eval_user_function(sym, a, context, x)?;
348                    stack.push(result);
349                } else {
350                    let a = stack.pop().ok_or(EvalError::StackUnderflow)?;
351                    let result = eval_unary(sym, a, context.trig_argument_scale)?;
352                    stack.push(result);
353                }
354            }
355            Seft::C => {
356                let b = stack.pop().ok_or(EvalError::StackUnderflow)?;
357                let a = stack.pop().ok_or(EvalError::StackUnderflow)?;
358                let result = eval_binary(sym, a, b)?;
359                stack.push(result);
360            }
361        }
362    }
363
364    if stack.len() != 1 {
365        return Err(EvalError::Invalid);
366    }
367
368    // SAFETY: len == 1 check above guarantees pop succeeds
369    let result = stack.pop().unwrap();
370
371    // Check for invalid results
372    if result.val.is_nan() || result.val.is_infinite() {
373        return Err(EvalError::Overflow);
374    }
375
376    Ok(EvalResult {
377        value: result.val,
378        derivative: result.deriv,
379        num_type: result.num_type,
380    })
381}
382
383/// Evaluate an expression at a given value of x.
384///
385/// Convenience wrapper that allocates a new workspace. For hot loops,
386/// prefer `evaluate_with_workspace()` with a reusable `EvalWorkspace`.
387///
388/// Note: This is a convenience API for library users. Internal code uses
389/// `evaluate_fast_with_constants_and_functions` for performance.
390pub fn evaluate(expr: &Expression, x: f64) -> Result<EvalResult, EvalError> {
391    evaluate_with_context(expr, x, &EvalContext::new())
392}
393
394/// Evaluate an expression at a given value of x with user constants.
395///
396/// Convenience wrapper that allocates a new workspace.
397pub fn evaluate_with_constants(
398    expr: &Expression,
399    x: f64,
400    user_constants: &[UserConstant],
401) -> Result<EvalResult, EvalError> {
402    let context = EvalContext::from_slices(user_constants, &[]);
403    evaluate_with_context(expr, x, &context)
404}
405
406/// Evaluate an expression at a given value of x with user constants and user functions.
407///
408/// Convenience wrapper that allocates a new workspace.
409pub fn evaluate_with_constants_and_functions(
410    expr: &Expression,
411    x: f64,
412    user_constants: &[UserConstant],
413    user_functions: &[UserFunction],
414) -> Result<EvalResult, EvalError> {
415    let context = EvalContext::from_slices(user_constants, user_functions);
416    evaluate_with_context(expr, x, &context)
417}
418
419/// Evaluate an expression at a given value of x with an explicit evaluation context.
420///
421/// Convenience wrapper that allocates a new workspace.
422pub fn evaluate_with_context(
423    expr: &Expression,
424    x: f64,
425    context: &EvalContext<'_>,
426) -> Result<EvalResult, EvalError> {
427    let mut workspace = EvalWorkspace::new();
428    evaluate_with_workspace_and_context(expr, x, &mut workspace, context)
429}
430
431/// Evaluate an expression using a thread-local workspace (zero allocations after warmup).
432///
433/// This is ideal for parallel code where each thread needs its own workspace.
434/// Note: This version does NOT support user constants. For user constants,
435/// use `evaluate_with_constants()` or `evaluate_with_workspace_and_constants()`.
436///
437/// Note: This is a convenience wrapper for the full `evaluate_fast_with_constants_and_functions`
438/// when you don't need user constants or functions. It's provided as a simpler API for common cases.
439#[inline]
440pub fn evaluate_fast(expr: &Expression, x: f64) -> Result<EvalResult, EvalError> {
441    evaluate_fast_with_context(expr, x, &EvalContext::new())
442}
443
444/// Evaluate an expression using a thread-local workspace with user constants.
445///
446/// Note: This uses a global thread-local storage, so it's not safe to call recursively
447/// with different user_constants. For recursive calls, use `evaluate_with_workspace_and_constants`.
448///
449/// Note: This is a convenience wrapper for the full `evaluate_fast_with_constants_and_functions`
450/// when you don't need user functions. It's provided as a simpler API for common cases.
451#[inline]
452pub fn evaluate_fast_with_constants(
453    expr: &Expression,
454    x: f64,
455    user_constants: &[UserConstant],
456) -> Result<EvalResult, EvalError> {
457    let context = EvalContext::from_slices(user_constants, &[]);
458    evaluate_fast_with_context(expr, x, &context)
459}
460
461/// Evaluate an expression using a thread-local workspace with user constants and user functions.
462///
463/// # Thread-Local Workspace
464///
465/// This function uses a `thread_local!` static to cache an `EvalWorkspace` for each thread.
466/// The workspace is created on first use and reused for all subsequent calls from the same thread.
467/// This provides zero-allocation evaluation after the initial warmup, making it ideal for:
468///
469/// - Parallel code where each thread needs its own workspace
470/// - Hot loops where allocation overhead matters
471/// - High-throughput evaluation scenarios
472///
473/// # Limitations
474///
475/// - This uses a global thread-local storage, so it's not safe to call recursively
476///   with different `user_constants` or `user_functions`. The same workspace is shared.
477/// - For recursive calls or when user constants/functions vary per-call,
478///   use [`evaluate_with_workspace_and_constants_and_functions`] instead.
479///
480/// # Example
481///
482/// ```no_run
483/// use ries_rs::eval::evaluate_fast_with_constants_and_functions;
484/// use ries_rs::expr::Expression;
485/// let expr = Expression::new();
486/// let x = 1.0_f64;
487/// // First call allocates workspace (warmup)
488/// let result = evaluate_fast_with_constants_and_functions(&expr, x, &[], &[]);
489///
490/// // Subsequent calls reuse the same workspace (no allocations)
491/// for _ in 0..1000 {
492///     let _ = evaluate_fast_with_constants_and_functions(&expr, x, &[], &[]);
493/// }
494/// ```
495#[inline]
496pub fn evaluate_fast_with_constants_and_functions(
497    expr: &Expression,
498    x: f64,
499    user_constants: &[UserConstant],
500    user_functions: &[UserFunction],
501) -> Result<EvalResult, EvalError> {
502    let context = EvalContext::from_slices(user_constants, user_functions);
503    evaluate_fast_with_context(expr, x, &context)
504}
505
506/// Evaluate an expression using a thread-local workspace and explicit context.
507#[inline]
508pub fn evaluate_fast_with_context(
509    expr: &Expression,
510    x: f64,
511    context: &EvalContext<'_>,
512) -> Result<EvalResult, EvalError> {
513    thread_local! {
514        /// Thread-local evaluation workspace.
515        ///
516        /// Each thread gets its own workspace instance that's lazily allocated
517        /// on first use. The workspace maintains internal Vec storage that grows
518        /// as needed but is never deallocated, providing zero-allocation hot paths.
519        static WORKSPACE: std::cell::RefCell<EvalWorkspace> = std::cell::RefCell::new(EvalWorkspace::new());
520    }
521
522    WORKSPACE.with(|ws| {
523        let mut workspace = ws.borrow_mut();
524        evaluate_with_workspace_and_context(expr, x, &mut workspace, context)
525    })
526}
527
528/// Evaluate a constant or variable symbol with user constant lookup.
529fn eval_constant_with_user(
530    sym: Symbol,
531    x: f64,
532    user_constants: &[UserConstant],
533) -> Result<StackEntry, EvalError> {
534    use Symbol::*;
535    match sym {
536        One => Ok(StackEntry::constant(1.0, NumType::Integer)),
537        Two => Ok(StackEntry::constant(2.0, NumType::Integer)),
538        Three => Ok(StackEntry::constant(3.0, NumType::Integer)),
539        Four => Ok(StackEntry::constant(4.0, NumType::Integer)),
540        Five => Ok(StackEntry::constant(5.0, NumType::Integer)),
541        Six => Ok(StackEntry::constant(6.0, NumType::Integer)),
542        Seven => Ok(StackEntry::constant(7.0, NumType::Integer)),
543        Eight => Ok(StackEntry::constant(8.0, NumType::Integer)),
544        Nine => Ok(StackEntry::constant(9.0, NumType::Integer)),
545        Pi => Ok(StackEntry::constant(constants::PI, NumType::Transcendental)),
546        E => Ok(StackEntry::constant(constants::E, NumType::Transcendental)),
547        Phi => Ok(StackEntry::constant(constants::PHI, NumType::Algebraic)),
548        // New constants
549        Gamma => Ok(StackEntry::constant(
550            constants::GAMMA,
551            NumType::Transcendental,
552        )),
553        Plastic => Ok(StackEntry::constant(constants::PLASTIC, NumType::Algebraic)),
554        Apery => Ok(StackEntry::constant(
555            constants::APERY,
556            NumType::Transcendental,
557        )),
558        Catalan => Ok(StackEntry::constant(
559            constants::CATALAN,
560            NumType::Transcendental,
561        )),
562        X => Ok(StackEntry::new(x, 1.0, NumType::Integer)), // x can be any value, including integer
563        // User constants - look up value from the user_constants slice
564        UserConstant0 | UserConstant1 | UserConstant2 | UserConstant3 | UserConstant4
565        | UserConstant5 | UserConstant6 | UserConstant7 | UserConstant8 | UserConstant9
566        | UserConstant10 | UserConstant11 | UserConstant12 | UserConstant13 | UserConstant14
567        | UserConstant15 => {
568            // Get the index from the symbol
569            let idx = sym.user_constant_index().unwrap() as usize;
570            user_constants
571                .get(idx)
572                .map(|uc| StackEntry::constant(uc.value, uc.num_type))
573                .ok_or(EvalError::MissingUserConstant(idx))
574        }
575        _ => Err(EvalError::Invalid),
576    }
577}
578
579/// Evaluate a user-defined function
580///
581/// Takes the input argument and the user_functions slice, looks up the function
582/// definition, executes the body, and returns the result.
583fn eval_user_function(
584    sym: Symbol,
585    input: StackEntry,
586    context: &EvalContext<'_>,
587    x: f64,
588) -> Result<StackEntry, EvalError> {
589    // Get the function index
590    let idx = sym.user_function_index().ok_or(EvalError::Invalid)? as usize;
591
592    // Look up the function definition
593    let udf = context.user_functions.get(idx).ok_or(EvalError::Invalid)?;
594
595    // Reuse a thread-local scratch buffer rather than allocating a fresh Vec on every
596    // call. eval_user_function is invoked in the inner generation loop (potentially
597    // millions of times at high complexity), so avoiding the heap allocation matters.
598    // UDFs do not call other UDFs, so the borrow is never re-entered.
599    thread_local! {
600        static UDF_STACK: std::cell::RefCell<Vec<StackEntry>> =
601            std::cell::RefCell::new(Vec::with_capacity(16));
602    }
603
604    UDF_STACK.with(|cell| -> Result<StackEntry, EvalError> {
605        let mut stack = cell.borrow_mut();
606        stack.clear();
607        stack.push(input);
608
609        // Execute each operation in the function body
610        for op in &udf.body {
611            match op {
612                UdfOp::Symbol(sym) => {
613                    match sym.seft() {
614                        Seft::A => {
615                            // Constant - push onto stack
616                            let entry = eval_constant_with_user(*sym, x, context.user_constants)?;
617                            stack.push(entry);
618                        }
619                        Seft::B => {
620                            // Unary operator - pop one, push result
621                            let a = stack.pop().ok_or(EvalError::StackUnderflow)?;
622                            let result = eval_unary(*sym, a, context.trig_argument_scale)?;
623                            stack.push(result);
624                        }
625                        Seft::C => {
626                            // Binary operator - pop two, push result
627                            let b = stack.pop().ok_or(EvalError::StackUnderflow)?;
628                            let a = stack.pop().ok_or(EvalError::StackUnderflow)?;
629                            let result = eval_binary(*sym, a, b)?;
630                            stack.push(result);
631                        }
632                    }
633                }
634                UdfOp::Dup => {
635                    // Duplicate top of stack. Dereference immediately so the
636                    // immutable borrow ends before the mutable push.
637                    let top = *stack.last().ok_or(EvalError::StackUnderflow)?;
638                    stack.push(top);
639                }
640                UdfOp::Swap => {
641                    // Swap top two elements
642                    let len = stack.len();
643                    if len < 2 {
644                        return Err(EvalError::StackUnderflow);
645                    }
646                    stack.swap(len - 1, len - 2);
647                }
648            }
649        }
650
651        // Function should leave exactly one value on the stack
652        if stack.len() != 1 {
653            return Err(EvalError::Invalid);
654        }
655
656        // SAFETY: len == 1 check above guarantees pop succeeds
657        let result = stack.pop().unwrap();
658
659        // Check for invalid results
660        if result.val.is_nan() || result.val.is_infinite() {
661            return Err(EvalError::Overflow);
662        }
663
664        Ok(result)
665    })
666}
667
668/// Evaluate a unary operator with derivative
669fn eval_unary(
670    sym: Symbol,
671    a: StackEntry,
672    trig_argument_scale: f64,
673) -> Result<StackEntry, EvalError> {
674    use Symbol::*;
675
676    #[inline]
677    fn is_default_trig_scale(scale: f64) -> bool {
678        (scale - DEFAULT_TRIG_ARGUMENT_SCALE).abs() <= f64::EPSILON
679    }
680
681    #[inline]
682    fn rounded_f64_to_i64(rounded: f64) -> Option<i64> {
683        if !rounded.is_finite() {
684            return None;
685        }
686        if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 {
687            return None;
688        }
689        Some(rounded as i64)
690    }
691
692    #[inline]
693    fn snap_integer(value: f64) -> Option<i64> {
694        let rounded = value.round();
695        if (value - rounded).abs() <= TRIG_EXACT_ARGUMENT_TOLERANCE {
696            rounded_f64_to_i64(rounded)
697        } else {
698            None
699        }
700    }
701
702    #[inline]
703    fn snap_half_integer_index(value: f64) -> Option<i64> {
704        let doubled = value * 2.0;
705        let rounded = doubled.round();
706        if (doubled - rounded).abs() > TRIG_EXACT_ARGUMENT_TOLERANCE {
707            return None;
708        }
709        let rounded_i = rounded_f64_to_i64(rounded)?;
710        if rounded_i.rem_euclid(2) == 0 {
711            return None;
712        }
713        Some((rounded_i - 1) / 2)
714    }
715
716    #[inline]
717    fn alternating_sign(n: i64) -> f64 {
718        if n.rem_euclid(2) == 0 {
719            1.0
720        } else {
721            -1.0
722        }
723    }
724
725    let (val, deriv, num_type) = match sym {
726        // Negation: -a, d(-a)/dx = -da/dx
727        Neg => (-a.val, -a.deriv, a.num_type),
728
729        // Reciprocal: 1/a, d(1/a)/dx = -da/dx / a²
730        Recip => {
731            if a.val.abs() < f64::MIN_POSITIVE {
732                return Err(EvalError::DivisionByZero);
733            }
734            let val = 1.0 / a.val;
735            let deriv = -a.deriv / (a.val * a.val);
736            let num_type = if a.num_type == NumType::Integer {
737                NumType::Rational
738            } else {
739                a.num_type
740            };
741            (val, deriv, num_type)
742        }
743
744        // Square root: sqrt(a), d(sqrt(a))/dx = da/dx / (2*sqrt(a))
745        Sqrt => {
746            if a.val < 0.0 {
747                return Err(EvalError::SqrtDomain);
748            }
749            let val = a.val.sqrt();
750            let deriv = if val.abs() > f64::MIN_POSITIVE {
751                a.deriv / (2.0 * val)
752            } else {
753                0.0
754            };
755            let num_type = if a.num_type >= NumType::Constructible {
756                NumType::Constructible
757            } else {
758                a.num_type
759            };
760            (val, deriv, num_type)
761        }
762
763        // Square: a², d(a²)/dx = 2*a*da/dx
764        Square => {
765            let val = a.val * a.val;
766            let deriv = 2.0 * a.val * a.deriv;
767            (val, deriv, a.num_type)
768        }
769
770        // Natural log: ln(a), d(ln(a))/dx = da/dx / a
771        Ln => {
772            if a.val <= 0.0 {
773                return Err(EvalError::LogDomain);
774            }
775            let val = a.val.ln();
776            let deriv = a.deriv / a.val;
777            (val, deriv, NumType::Transcendental)
778        }
779
780        // Exponential: e^a, d(e^a)/dx = e^a * da/dx
781        Exp => {
782            let val = a.val.exp();
783            if val.is_infinite() {
784                return Err(EvalError::Overflow);
785            }
786            let deriv = val * a.deriv;
787            (val, deriv, NumType::Transcendental)
788        }
789
790        // sin(π*a), d(sin(πa))/dx = π*cos(πa)*da/dx
791        SinPi => {
792            if is_default_trig_scale(trig_argument_scale) {
793                if let Some(n) = snap_integer(a.val) {
794                    (
795                        0.0,
796                        trig_argument_scale * alternating_sign(n) * a.deriv,
797                        NumType::Transcendental,
798                    )
799                } else if let Some(n) = snap_half_integer_index(a.val) {
800                    (alternating_sign(n), 0.0, NumType::Transcendental)
801                } else {
802                    let val = (trig_argument_scale * a.val).sin();
803                    let deriv = trig_argument_scale * (trig_argument_scale * a.val).cos() * a.deriv;
804                    (val, deriv, NumType::Transcendental)
805                }
806            } else {
807                let val = (trig_argument_scale * a.val).sin();
808                let deriv = trig_argument_scale * (trig_argument_scale * a.val).cos() * a.deriv;
809                (val, deriv, NumType::Transcendental)
810            }
811        }
812
813        // cos(π*a), d(cos(πa))/dx = -π*sin(πa)*da/dx
814        CosPi => {
815            if is_default_trig_scale(trig_argument_scale) {
816                if let Some(n) = snap_integer(a.val) {
817                    (alternating_sign(n), 0.0, NumType::Transcendental)
818                } else if let Some(n) = snap_half_integer_index(a.val) {
819                    (
820                        0.0,
821                        -trig_argument_scale * alternating_sign(n) * a.deriv,
822                        NumType::Transcendental,
823                    )
824                } else {
825                    let val = (trig_argument_scale * a.val).cos();
826                    let deriv =
827                        -trig_argument_scale * (trig_argument_scale * a.val).sin() * a.deriv;
828                    (val, deriv, NumType::Transcendental)
829                }
830            } else {
831                let val = (trig_argument_scale * a.val).cos();
832                let deriv = -trig_argument_scale * (trig_argument_scale * a.val).sin() * a.deriv;
833                (val, deriv, NumType::Transcendental)
834            }
835        }
836
837        // tan(π*a), d(tan(πa))/dx = π*sec²(πa)*da/dx
838        TanPi => {
839            if is_default_trig_scale(trig_argument_scale) {
840                if snap_half_integer_index(a.val).is_some() {
841                    return Err(EvalError::Overflow);
842                }
843                if snap_integer(a.val).is_some() {
844                    (0.0, trig_argument_scale * a.deriv, NumType::Transcendental)
845                } else {
846                    let cos_val = (trig_argument_scale * a.val).cos();
847                    if cos_val.abs() < 1e-10 {
848                        return Err(EvalError::Overflow);
849                    }
850                    let val = (trig_argument_scale * a.val).tan();
851                    let deriv = trig_argument_scale * a.deriv / (cos_val * cos_val);
852                    (val, deriv, NumType::Transcendental)
853                }
854            } else {
855                let cos_val = (trig_argument_scale * a.val).cos();
856                if cos_val.abs() < 1e-10 {
857                    return Err(EvalError::Overflow);
858                }
859                let val = (trig_argument_scale * a.val).tan();
860                let deriv = trig_argument_scale * a.deriv / (cos_val * cos_val);
861                (val, deriv, NumType::Transcendental)
862            }
863        }
864
865        // Lambert W function (principal branch)
866        LambertW => {
867            let val = lambert_w(a.val)?;
868            // d(W(a))/dx = W(a) / (a * (1 + W(a))) * da/dx
869            // Special case: W'(0) = 1 (by L'Hôpital's rule, since W(x) ≈ x near 0)
870            let deriv = if a.val.abs() < 1e-10 {
871                a.deriv // W'(0) = 1
872            } else {
873                let denom = a.val * (1.0 + val);
874                if denom.abs() > f64::MIN_POSITIVE {
875                    val / denom * a.deriv
876                } else {
877                    0.0
878                }
879            };
880            (val, deriv, NumType::Transcendental)
881        }
882
883        // User functions are handled at the main evaluation loop level, not here
884        // If we reach this point, return an error
885        UserFunction0 | UserFunction1 | UserFunction2 | UserFunction3 | UserFunction4
886        | UserFunction5 | UserFunction6 | UserFunction7 | UserFunction8 | UserFunction9
887        | UserFunction10 | UserFunction11 | UserFunction12 | UserFunction13 | UserFunction14
888        | UserFunction15 => {
889            // This indicates a bug in the evaluation loop - user functions should be
890            // handled before calling eval_unary
891            return Err(EvalError::Invalid);
892        }
893
894        // Non-unary symbols should never be passed to this function
895        _ => return Err(EvalError::Invalid),
896    };
897
898    Ok(StackEntry::new(val, deriv, num_type))
899}
900
901/// Evaluate a binary operator with derivative
902fn eval_binary(sym: Symbol, a: StackEntry, b: StackEntry) -> Result<StackEntry, EvalError> {
903    use Symbol::*;
904
905    let (val, deriv, num_type) = match sym {
906        // Addition: a + b
907        Add => {
908            let val = a.val + b.val;
909            let deriv = a.deriv + b.deriv;
910            let num_type = a.num_type.combine(b.num_type);
911            (val, deriv, num_type)
912        }
913
914        // Subtraction: a - b
915        Sub => {
916            let val = a.val - b.val;
917            let deriv = a.deriv - b.deriv;
918            let num_type = a.num_type.combine(b.num_type);
919            (val, deriv, num_type)
920        }
921
922        // Multiplication: a * b, d(ab)/dx = a*db/dx + b*da/dx
923        Mul => {
924            let val = a.val * b.val;
925            let deriv = a.val * b.deriv + b.val * a.deriv;
926            let num_type = a.num_type.combine(b.num_type);
927            (val, deriv, num_type)
928        }
929
930        // Division: a / b, d(a/b)/dx = (b*da/dx - a*db/dx) / b²
931        Div => {
932            if b.val.abs() < f64::MIN_POSITIVE {
933                return Err(EvalError::DivisionByZero);
934            }
935            let val = a.val / b.val;
936            let deriv = (b.val * a.deriv - a.val * b.deriv) / (b.val * b.val);
937            let mut num_type = a.num_type.combine(b.num_type);
938            if num_type == NumType::Integer {
939                num_type = NumType::Rational;
940            }
941            (val, deriv, num_type)
942        }
943
944        // Power: a^b, d(a^b)/dx = a^b * (b*da/dx/a + ln(a)*db/dx)
945        Pow => {
946            if a.val <= 0.0 && b.val.fract() != 0.0 {
947                return Err(EvalError::SqrtDomain);
948            }
949            let val = a.val.powf(b.val);
950            if val.is_infinite() || val.is_nan() {
951                return Err(EvalError::Overflow);
952            }
953            // Guard for near-zero base to avoid numerical issues
954            let deriv = if a.val > f64::MIN_POSITIVE {
955                val * (b.val * a.deriv / a.val + a.val.ln() * b.deriv)
956            } else if a.val.abs() < f64::MIN_POSITIVE && b.val > 0.0 {
957                0.0
958            } else {
959                // Negative base, integer exponent (or near-zero base treated as 0).
960                // Full formula: val * (b * a.deriv/a + ln(a) * b.deriv).
961                // The ln(a) * b.deriv term is intentionally dropped here: ln(negative) is
962                // undefined in the reals (NaN), so it cannot contribute to Newton-Raphson.
963                // Dropping it gives 0 for the derivative w.r.t. x-in-the-exponent path,
964                // which is the correct safe fallback when x appears in the exponent of a
965                // negative base (e.g., (-2)^x is only real-valued at integer x).
966                if a.val.abs() < f64::MIN_POSITIVE {
967                    0.0
968                } else {
969                    val * b.val * a.deriv / a.val
970                }
971            };
972            let num_type = if b.num_type == NumType::Integer {
973                a.num_type
974            } else {
975                NumType::Transcendental
976            };
977            (val, deriv, num_type)
978        }
979
980        // a-th root of b: b^(1/a)
981        Root => {
982            if a.val.abs() < f64::MIN_POSITIVE {
983                return Err(EvalError::DivisionByZero);
984            }
985            let exp = 1.0 / a.val;
986
987            // For negative radicands, we need to check if the index is an odd integer
988            // Non-integer indices of negative numbers have no real value
989            if b.val < 0.0 {
990                // Check if the index is close to an integer
991                let rounded = a.val.round();
992                let is_integer = (a.val - rounded).abs() < 1e-10;
993
994                if !is_integer {
995                    // Non-integer index of negative number - no real value
996                    return Err(EvalError::SqrtDomain);
997                }
998
999                // Check if the integer is odd (odd roots of negatives are real)
1000                let int_val = rounded as i64;
1001                if int_val % 2 == 0 {
1002                    // Even integer root of negative - no real value
1003                    return Err(EvalError::SqrtDomain);
1004                }
1005                // Odd integer root of negative is OK
1006            }
1007
1008            let val = if b.val < 0.0 {
1009                // Odd root of negative number
1010                -((-b.val).powf(exp))
1011            } else {
1012                b.val.powf(exp)
1013            };
1014            if val.is_infinite() || val.is_nan() {
1015                return Err(EvalError::Overflow);
1016            }
1017            // d(b^(1/a))/dx = b^(1/a) * (db/dx/(a*b) - ln(b)*da/dx/a²)
1018            let deriv = if b.val.abs() > f64::MIN_POSITIVE {
1019                val * (b.deriv / (a.val * b.val) - b.val.abs().ln() * a.deriv / (a.val * a.val))
1020            } else {
1021                0.0
1022            };
1023            (val, deriv, NumType::Algebraic)
1024        }
1025
1026        // Logarithm base a of b: ln(b) / ln(a)
1027        Log => {
1028            if a.val <= 0.0 || a.val == 1.0 || b.val <= 0.0 {
1029                return Err(EvalError::LogDomain);
1030            }
1031            let ln_a = a.val.ln();
1032            let ln_b = b.val.ln();
1033            let val = ln_b / ln_a;
1034            // d(log_a(b))/dx = (db/dx/(b*ln(a)) - ln(b)*da/dx/(a*ln(a)²))
1035            let deriv = b.deriv / (b.val * ln_a) - ln_b * a.deriv / (a.val * ln_a * ln_a);
1036            (val, deriv, NumType::Transcendental)
1037        }
1038
1039        // atan2(a, b) = angle of point (b, a) from origin
1040        Atan2 => {
1041            let val = a.val.atan2(b.val);
1042            // d(atan2(a,b))/dx = (b*da/dx - a*db/dx) / (a² + b²)
1043            let denom = a.val * a.val + b.val * b.val;
1044            let deriv = if denom.abs() > f64::MIN_POSITIVE {
1045                (b.val * a.deriv - a.val * b.deriv) / denom
1046            } else {
1047                0.0
1048            };
1049            (val, deriv, NumType::Transcendental)
1050        }
1051
1052        // Non-binary symbols should never be passed to this function
1053        _ => return Err(EvalError::Invalid),
1054    };
1055
1056    Ok(StackEntry::new(val, deriv, num_type))
1057}
1058
1059/// Compute the Lambert W function (principal branch) using Halley's method
1060///
1061/// The Lambert W function satisfies W(x) * exp(W(x)) = x.
1062/// This implementation handles the principal branch (W₀) for x ≥ -1/e.
1063fn lambert_w(x: f64) -> Result<f64, EvalError> {
1064    // Branch point: x = -1/e gives W = -1
1065    const INV_E: f64 = 1.0 / std::f64::consts::E;
1066    const NEG_INV_E: f64 = -INV_E; // -0.36787944117144233...
1067
1068    // Domain check
1069    if x < NEG_INV_E {
1070        return Err(EvalError::LogDomain);
1071    }
1072
1073    // Special cases
1074    if x == 0.0 {
1075        return Ok(0.0); // W(0) = 0
1076    }
1077    if (x - NEG_INV_E).abs() < 1e-15 {
1078        return Ok(-1.0); // W(-1/e) = -1
1079    }
1080    if x == constants::E {
1081        return Ok(1.0); // W(e) = 1
1082    }
1083
1084    // Initial guess - different approximations for different regimes
1085    let mut w = if x < -0.3 {
1086        // Near the branch point, use a series expansion around -1/e
1087        // W(x) ≈ -1 + p - p²/3 + 11p³/72 where p = sqrt(2(ex + 1))
1088        let p = (2.0 * (constants::E * x + 1.0)).sqrt();
1089        -1.0 + p * (1.0 - p / 3.0 * (1.0 - 11.0 * p / 72.0))
1090    } else if x < 0.25 {
1091        // Near zero, use a polynomial approximation
1092        // W(x) ≈ x - x² + 3x³/2 - 8x⁴/3 + ...
1093        // For numerical stability, use a rational approximation
1094        let x2 = x * x;
1095        x * (1.0 - x + x2 * (1.5 - 2.6667 * x))
1096    } else if x < 4.0 {
1097        // Moderate range: use log-based approximation
1098        // W(x) ≈ ln(x) - ln(ln(x)) + ln(ln(x))/ln(x)
1099        let lnx = x.ln();
1100        if lnx > 0.0 {
1101            let lnlnx = lnx.ln().max(0.0);
1102            lnx - lnlnx + lnlnx / lnx.max(1.0)
1103        } else {
1104            x // fallback for x near 1
1105        }
1106    } else {
1107        // Large x: W(x) ≈ ln(x) - ln(ln(x)) + ln(ln(x))/ln(x)
1108        let l1 = x.ln();
1109        let l2 = l1.ln();
1110        l1 - l2 + l2 / l1
1111    };
1112
1113    // Halley's method iteration
1114    // For well-chosen initial guesses, 10-15 iterations are usually enough
1115    for _ in 0..25 {
1116        let ew = w.exp();
1117
1118        // Handle potential overflow
1119        if !ew.is_finite() {
1120            // Back off to a more stable approach
1121            w = x.ln() - w.ln().max(1e-10);
1122            continue;
1123        }
1124
1125        let wew = w * ew;
1126        let diff = wew - x;
1127
1128        // Convergence check with relative tolerance
1129        let tol = 1e-15 * (1.0 + w.abs().max(x.abs()));
1130        if diff.abs() < tol {
1131            break;
1132        }
1133
1134        let w1 = w + 1.0;
1135        // Halley's correction
1136        let denom = ew * w1 - 0.5 * (w + 2.0) * diff / w1;
1137        if denom.abs() < f64::MIN_POSITIVE {
1138            break;
1139        }
1140
1141        let delta = diff / denom;
1142
1143        // Damping for stability near branch point
1144        let correction = if w < -0.5 && delta.abs() > 0.5 {
1145            delta * 0.5 // Damped update near branch point
1146        } else {
1147            delta
1148        };
1149
1150        w -= correction;
1151    }
1152
1153    // Final validation
1154    if !w.is_finite() {
1155        return Err(EvalError::Overflow);
1156    }
1157
1158    Ok(w)
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163    use super::*;
1164
1165    fn approx_eq(a: f64, b: f64) -> bool {
1166        (a - b).abs() < 1e-10
1167    }
1168
1169    #[test]
1170    fn test_basic_eval() {
1171        let expr = Expression::parse("32+").unwrap();
1172        let result = evaluate(&expr, 0.0).unwrap();
1173        assert!(approx_eq(result.value, 5.0));
1174        assert!(approx_eq(result.derivative, 0.0));
1175    }
1176
1177    #[test]
1178    fn test_variable() {
1179        let expr = Expression::parse("x").unwrap();
1180        let result = evaluate(&expr, 3.5).unwrap();
1181        assert!(approx_eq(result.value, 3.5));
1182        assert!(approx_eq(result.derivative, 1.0));
1183    }
1184
1185    #[test]
1186    fn test_x_squared() {
1187        let expr = Expression::parse("xs").unwrap(); // x^2
1188        let result = evaluate(&expr, 3.0).unwrap();
1189        assert!(approx_eq(result.value, 9.0));
1190        assert!(approx_eq(result.derivative, 6.0)); // 2x
1191    }
1192
1193    #[test]
1194    fn test_sqrt_pi() {
1195        let expr = Expression::parse("pq").unwrap(); // sqrt(pi)
1196        let result = evaluate(&expr, 0.0).unwrap();
1197        assert!(approx_eq(result.value, constants::PI.sqrt()));
1198    }
1199
1200    #[test]
1201    fn test_e_to_x() {
1202        let expr = Expression::parse("xE").unwrap(); // e^x
1203        let result = evaluate(&expr, 1.0).unwrap();
1204        assert!(approx_eq(result.value, constants::E));
1205        assert!(approx_eq(result.derivative, constants::E)); // d(e^x)/dx = e^x
1206    }
1207
1208    #[test]
1209    fn test_complex_expr() {
1210        // x^2 + 2*x + 1 = (x+1)^2
1211        let expr = Expression::parse("xs2x*+1+").unwrap();
1212        let result = evaluate(&expr, 3.0).unwrap();
1213        assert!(approx_eq(result.value, 16.0)); // (3+1)^2
1214        assert!(approx_eq(result.derivative, 8.0)); // 2x + 2 = 8
1215    }
1216
1217    #[test]
1218    fn test_lambert_w() {
1219        // W(1) ≈ 0.5671432904
1220        let w = lambert_w(1.0).unwrap();
1221        assert!((w - 0.5671432904).abs() < 1e-9);
1222
1223        // W(e) = 1
1224        let w = lambert_w(constants::E).unwrap();
1225        assert!((w - 1.0).abs() < 1e-10);
1226    }
1227
1228    #[test]
1229    fn test_sinpi_snaps_exact_integer_and_half_integer_arguments() {
1230        let integer_expr = Expression::parse("xS").unwrap();
1231        let integer_result = evaluate(&integer_expr, 1.0).unwrap();
1232        assert_eq!(integer_result.value, 0.0);
1233        assert!(approx_eq(integer_result.derivative, -constants::PI));
1234
1235        let half_integer_expr = Expression::parse("12/S").unwrap();
1236        let half_integer_result = evaluate(&half_integer_expr, 0.0).unwrap();
1237        assert_eq!(half_integer_result.value, 1.0);
1238        assert_eq!(half_integer_result.derivative, 0.0);
1239    }
1240
1241    #[test]
1242    fn test_cospi_snaps_exact_integer_and_half_integer_arguments() {
1243        let integer_expr = Expression::parse("xC").unwrap();
1244        let integer_result = evaluate(&integer_expr, 1.0).unwrap();
1245        assert_eq!(integer_result.value, -1.0);
1246        assert_eq!(integer_result.derivative, 0.0);
1247
1248        let half_integer_expr = Expression::parse("xC").unwrap();
1249        let half_integer_result = evaluate(&half_integer_expr, 0.5).unwrap();
1250        assert_eq!(half_integer_result.value, 0.0);
1251        assert!(approx_eq(half_integer_result.derivative, -constants::PI));
1252    }
1253
1254    #[test]
1255    fn test_tanpi_snaps_exact_integer_and_half_integer_arguments() {
1256        let integer_expr = Expression::parse("xT").unwrap();
1257        let integer_result = evaluate(&integer_expr, 1.0).unwrap();
1258        assert_eq!(integer_result.value, 0.0);
1259        assert!(approx_eq(integer_result.derivative, constants::PI));
1260
1261        let half_integer_expr = Expression::parse("12/T").unwrap();
1262        assert!(matches!(
1263            evaluate(&half_integer_expr, 0.0),
1264            Err(EvalError::Overflow)
1265        ));
1266    }
1267
1268    #[test]
1269    fn test_custom_trig_argument_scale_preserves_raw_trig_behavior() {
1270        let expr = Expression::parse("xS").unwrap();
1271        let context = EvalContext::new().with_trig_argument_scale(1.0);
1272        let result = evaluate_with_context(&expr, 1.0, &context).unwrap();
1273
1274        assert!(approx_eq(result.value, 1.0_f64.sin()));
1275        assert!(approx_eq(result.derivative, 1.0_f64.cos()));
1276    }
1277
1278    #[test]
1279    fn test_exact_trig_zero_rejects_pathological_negative_power_branch() {
1280        let expr = Expression::parse("1Sxn^S").unwrap();
1281        assert!(matches!(
1282            evaluate(&expr, 2.506314),
1283            Err(EvalError::SqrtDomain)
1284        ));
1285    }
1286
1287    #[test]
1288    fn test_user_constant_evaluation() {
1289        use crate::profile::UserConstant;
1290
1291        // Create a user constant (Euler-Mascheroni gamma ≈ 0.57721)
1292        let user_constants = vec![UserConstant {
1293            weight: 8,
1294            name: "g".to_string(),
1295            description: "gamma".to_string(),
1296            value: 0.5772156649,
1297            num_type: NumType::Transcendental,
1298        }];
1299
1300        // Create expression with UserConstant0 (byte 128)
1301        let expr = Expression::from_symbols(&[Symbol::UserConstant0]);
1302
1303        // Evaluate with user constants
1304        let result = evaluate_with_constants(&expr, 0.0, &user_constants).unwrap();
1305
1306        // Should match the user constant value
1307        assert!(approx_eq(result.value, 0.5772156649));
1308        // Derivative should be 0 (it's a constant)
1309        assert!(approx_eq(result.derivative, 0.0));
1310    }
1311
1312    #[test]
1313    fn test_user_constant_in_expression() {
1314        use crate::profile::UserConstant;
1315
1316        // Create two user constants
1317        let user_constants = vec![
1318            UserConstant {
1319                weight: 8,
1320                name: "a".to_string(),
1321                description: "constant a".to_string(),
1322                value: 2.0,
1323                num_type: NumType::Integer,
1324            },
1325            UserConstant {
1326                weight: 8,
1327                name: "b".to_string(),
1328                description: "constant b".to_string(),
1329                value: 3.0,
1330                num_type: NumType::Integer,
1331            },
1332        ];
1333
1334        // Create expression: u0 * x + u1 (in postfix: u0 x * u1 +)
1335        let expr = Expression::from_symbols(&[
1336            Symbol::UserConstant0,
1337            Symbol::X,
1338            Symbol::Mul,
1339            Symbol::UserConstant1,
1340            Symbol::Add,
1341        ]);
1342
1343        // At x=4, should be 2*4 + 3 = 11
1344        let result = evaluate_with_constants(&expr, 4.0, &user_constants).unwrap();
1345        assert!(approx_eq(result.value, 11.0));
1346        // Derivative should be 2 (from u0 * x)
1347        assert!(approx_eq(result.derivative, 2.0));
1348    }
1349
1350    #[test]
1351    fn test_user_constant_missing_returns_error() {
1352        // Missing user constant slots must fail explicitly instead of silently
1353        // changing the expression's meaning.
1354        let expr = Expression::from_symbols(&[Symbol::UserConstant0]);
1355
1356        let result = evaluate_with_constants(&expr, 0.0, &[]);
1357        assert!(matches!(result, Err(EvalError::MissingUserConstant(0))));
1358    }
1359
1360    #[test]
1361    fn test_user_function_sinh() {
1362        use crate::udf::UserFunction;
1363
1364        // sinh(x) = (e^x - e^-x) / 2
1365        // In postfix: E|r-2/ (exp, dup, recip, subtract, 2, divide)
1366        let user_functions = vec![UserFunction::parse("4:sinh:hyperbolic sine:E|r-2/").unwrap()];
1367
1368        // Create expression: sinh(x) (in postfix: xF0 where F0 = UserFunction0)
1369        let expr = Expression::from_symbols(&[Symbol::X, Symbol::UserFunction0]);
1370
1371        // sinh(1) = (e - e^-1) / 2 ≈ 1.1752
1372        let result =
1373            evaluate_with_constants_and_functions(&expr, 1.0, &[], &user_functions).unwrap();
1374        let expected = (constants::E - 1.0 / constants::E) / 2.0;
1375        assert!(approx_eq(result.value, expected));
1376
1377        // Derivative: d(sinh(x))/dx = cosh(x) = (e^x + e^-x) / 2
1378        let expected_deriv = (constants::E + 1.0 / constants::E) / 2.0;
1379        assert!((result.derivative - expected_deriv).abs() < 1e-10);
1380    }
1381
1382    #[test]
1383    fn test_user_function_xex() {
1384        use crate::udf::UserFunction;
1385
1386        // XeX(x) = x * e^x
1387        // In postfix: |E* (dup, exp, multiply)
1388        let user_functions = vec![UserFunction::parse("4:XeX:x*exp(x):|E*").unwrap()];
1389
1390        // Create expression: XeX(x) (in postfix: xF0 where F0 = UserFunction0)
1391        let expr = Expression::from_symbols(&[Symbol::X, Symbol::UserFunction0]);
1392
1393        // XeX(1) = 1 * e^1 = e
1394        let result =
1395            evaluate_with_constants_and_functions(&expr, 1.0, &[], &user_functions).unwrap();
1396        assert!(approx_eq(result.value, constants::E));
1397
1398        // Derivative: d(x*e^x)/dx = e^x + x*e^x = e^x * (1 + x) = e * 2
1399        let expected_deriv = constants::E * 2.0;
1400        assert!((result.derivative - expected_deriv).abs() < 1e-10);
1401    }
1402
1403    #[test]
1404    fn test_user_function_missing_returns_error() {
1405        // When no user functions are provided, user function evaluation should fail
1406        let expr = Expression::from_symbols(&[Symbol::X, Symbol::UserFunction0]);
1407
1408        let result = evaluate_with_constants_and_functions(&expr, 1.0, &[], &[]);
1409        assert!(result.is_err());
1410    }
1411}