Skip to main content

alkahest_cas/eval/
mod.rs

1//! Unified expression evaluation facade.
2//!
3//! The underlying evaluators deliberately retain their native representations:
4//! exact rationals stay exact, `f64` remains a fast approximate mode, and
5//! [`IntervalEval`] provides rigorous enclosures.  This module gives callers a
6//! single dispatch point and reports unsupported constructs structurally.
7
8mod complex_f64;
9
10use crate::ball::{ArbBall, IntervalEval};
11use crate::kernel::expr::PredicateKind;
12use crate::kernel::{ExprData, ExprId, ExprPool};
13use rug::Rational;
14use std::collections::HashMap;
15use std::fmt;
16
17pub use complex_f64::{eval_complex_f64, ComplexF64};
18
19/// Input bindings and representation selected for an evaluation.
20///
21/// Complex evaluation is intentionally a separate entry point
22/// ([`eval_complex_f64`]) so this enum stays semver-compatible without a
23/// major bump when the complex path lands.
24pub enum EvalMode<'a> {
25    /// Exact evaluation over rational numbers.  Float literals and
26    /// transcendental functions are rejected.
27    ExactRational(&'a HashMap<ExprId, Rational>),
28    /// Fast approximate evaluation using IEEE-754 double precision.
29    F64(&'a HashMap<ExprId, f64>),
30    /// Rigorous ball evaluation through the existing [`IntervalEval`] engine.
31    Interval(&'a IntervalEval),
32}
33
34/// Value returned by [`evaluate`].
35#[derive(Clone, Debug, PartialEq)]
36pub enum EvalValue {
37    Rational(Rational),
38    F64(f64),
39    Interval(ArbBall),
40}
41
42/// A structured reason why an expression cannot be evaluated in a mode.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum UnsupportedReason {
45    UnboundSymbol {
46        symbol: ExprId,
47    },
48    FloatLiteralInExactMode,
49    NonIntegerExponent,
50    ZeroToNegativePower,
51    UnsupportedFunction {
52        name: String,
53    },
54    UnsupportedExpression {
55        kind: &'static str,
56    },
57    InvalidPredicateArity {
58        kind: PredicateKind,
59        expected: usize,
60        actual: usize,
61    },
62    IndeterminatePredicate,
63    NonFiniteResult,
64    IntervalEvaluationFailed,
65}
66
67impl UnsupportedReason {
68    /// Stable machine-readable code for an unsupported evaluation outcome.
69    pub const fn code(&self) -> &'static str {
70        match self {
71            Self::UnboundSymbol { .. } => "E-EVAL-001",
72            Self::FloatLiteralInExactMode => "E-EVAL-002",
73            Self::NonIntegerExponent => "E-EVAL-003",
74            Self::ZeroToNegativePower => "E-EVAL-004",
75            Self::UnsupportedFunction { .. } => "E-EVAL-005",
76            Self::UnsupportedExpression { .. } => "E-EVAL-006",
77            Self::InvalidPredicateArity { .. } => "E-EVAL-007",
78            Self::IndeterminatePredicate => "E-EVAL-008",
79            Self::NonFiniteResult => "E-EVAL-009",
80            Self::IntervalEvaluationFailed => "E-EVAL-010",
81        }
82    }
83
84    /// Agent-facing error code, including complex branch-cut declines that
85    /// reuse [`UnsupportedReason::UnsupportedExpression`] without a breaking enum variant.
86    pub fn agent_code(&self) -> &'static str {
87        match self {
88            Self::UnsupportedExpression { kind: "branch_cut" } => "E-EVAL-011",
89            other => other.code(),
90        }
91    }
92}
93
94/// Evaluation failed because the requested mode cannot represent an operation
95/// or establish a required precondition.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct EvalError {
98    pub reason: UnsupportedReason,
99}
100
101impl fmt::Display for EvalError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "evaluation unsupported: {:?}", self.reason)
104    }
105}
106
107impl std::error::Error for EvalError {}
108
109/// Evaluate an expression in the representation selected by `mode`.
110pub fn evaluate(expr: ExprId, pool: &ExprPool, mode: EvalMode<'_>) -> Result<EvalValue, EvalError> {
111    match mode {
112        EvalMode::ExactRational(bindings) => {
113            eval_exact_rational(expr, pool, bindings).map(EvalValue::Rational)
114        }
115        EvalMode::F64(bindings) => eval_f64(expr, pool, bindings).map(EvalValue::F64),
116        EvalMode::Interval(eval) => eval
117            .eval(expr, pool)
118            .map(EvalValue::Interval)
119            .ok_or(error(UnsupportedReason::IntervalEvaluationFailed)),
120    }
121}
122
123/// Evaluate using exact rational arithmetic.
124pub fn eval_exact_rational(
125    expr: ExprId,
126    pool: &ExprPool,
127    bindings: &HashMap<ExprId, Rational>,
128) -> Result<Rational, EvalError> {
129    eval_rational_node(expr, pool, bindings)
130}
131
132/// Evaluate using IEEE-754 double precision.
133pub fn eval_f64(
134    expr: ExprId,
135    pool: &ExprPool,
136    bindings: &HashMap<ExprId, f64>,
137) -> Result<f64, EvalError> {
138    let result = eval_f64_node(expr, pool, bindings)?;
139    if result.is_finite() {
140        Ok(result)
141    } else {
142        Err(error(UnsupportedReason::NonFiniteResult))
143    }
144}
145
146/// Evaluate using the existing rigorous interval evaluator.
147pub fn eval_interval(
148    expr: ExprId,
149    pool: &ExprPool,
150    eval: &IntervalEval,
151) -> Result<ArbBall, EvalError> {
152    evaluate(expr, pool, EvalMode::Interval(eval)).map(|value| match value {
153        EvalValue::Interval(ball) => ball,
154        _ => unreachable!("interval mode always returns an interval"),
155    })
156}
157
158fn error(reason: UnsupportedReason) -> EvalError {
159    EvalError { reason }
160}
161
162fn eval_rational_node(
163    expr: ExprId,
164    pool: &ExprPool,
165    bindings: &HashMap<ExprId, Rational>,
166) -> Result<Rational, EvalError> {
167    match pool.get(expr) {
168        ExprData::Integer(n) => Ok(Rational::from(n.0.clone())),
169        ExprData::Rational(r) => Ok(r.0.clone()),
170        ExprData::Float(_) => Err(error(UnsupportedReason::FloatLiteralInExactMode)),
171        ExprData::Symbol { .. } => bindings
172            .get(&expr)
173            .cloned()
174            .ok_or(error(UnsupportedReason::UnboundSymbol { symbol: expr })),
175        ExprData::Add(args) => {
176            let mut sum = Rational::from(0);
177            for arg in args {
178                sum += eval_rational_node(arg, pool, bindings)?;
179            }
180            Ok(sum)
181        }
182        ExprData::Mul(args) => {
183            let mut product = Rational::from(1);
184            for arg in args {
185                product *= eval_rational_node(arg, pool, bindings)?;
186            }
187            Ok(product)
188        }
189        ExprData::Pow { base, exp } => {
190            let base = eval_rational_node(base, pool, bindings)?;
191            let exponent = integer_exponent(exp, pool)?;
192            rational_pow(base, exponent)
193        }
194        ExprData::Piecewise { branches, default } => {
195            for (condition, value) in branches {
196                if eval_rational_predicate(condition, pool, bindings)? {
197                    return eval_rational_node(value, pool, bindings);
198                }
199            }
200            eval_rational_node(default, pool, bindings)
201        }
202        ExprData::Predicate { .. } => Ok(Rational::from(eval_rational_predicate(
203            expr, pool, bindings,
204        )? as i32)),
205        ExprData::Func { name, .. } => Err(error(UnsupportedReason::UnsupportedFunction {
206            name: name.clone(),
207        })),
208        other => Err(error(UnsupportedReason::UnsupportedExpression {
209            kind: expr_kind(&other),
210        })),
211    }
212}
213
214fn integer_exponent(expr: ExprId, pool: &ExprPool) -> Result<i64, EvalError> {
215    match pool.get(expr) {
216        ExprData::Integer(n) => {
217            n.0.to_i64()
218                .ok_or(error(UnsupportedReason::NonIntegerExponent))
219        }
220        ExprData::Rational(r) if *r.0.denom() == 1 => {
221            r.0.numer()
222                .to_i64()
223                .ok_or(error(UnsupportedReason::NonIntegerExponent))
224        }
225        _ => Err(error(UnsupportedReason::NonIntegerExponent)),
226    }
227}
228
229fn rational_pow(mut base: Rational, exponent: i64) -> Result<Rational, EvalError> {
230    if exponent < 0 && base == 0 {
231        return Err(error(UnsupportedReason::ZeroToNegativePower));
232    }
233    let mut result = Rational::from(1);
234    let mut power = exponent.unsigned_abs();
235    while power != 0 {
236        if power & 1 == 1 {
237            result *= &base;
238        }
239        power >>= 1;
240        if power != 0 {
241            base *= base.clone();
242        }
243    }
244    if exponent < 0 {
245        Ok(Rational::from(1) / result)
246    } else {
247        Ok(result)
248    }
249}
250
251fn eval_rational_predicate(
252    expr: ExprId,
253    pool: &ExprPool,
254    bindings: &HashMap<ExprId, Rational>,
255) -> Result<bool, EvalError> {
256    let ExprData::Predicate { kind, args } = pool.get(expr) else {
257        return Err(error(UnsupportedReason::IndeterminatePredicate));
258    };
259    match kind {
260        PredicateKind::True => check_arity(&kind, &args, 0).map(|_| true),
261        PredicateKind::False => check_arity(&kind, &args, 0).map(|_| false),
262        PredicateKind::Not => Ok(!eval_rational_predicate(
263            predicate_arg(&kind, &args, 0)?,
264            pool,
265            bindings,
266        )?),
267        PredicateKind::And => {
268            for &arg in &args {
269                if !eval_rational_predicate(arg, pool, bindings)? {
270                    return Ok(false);
271                }
272            }
273            Ok(true)
274        }
275        PredicateKind::Or => {
276            for &arg in &args {
277                if eval_rational_predicate(arg, pool, bindings)? {
278                    return Ok(true);
279                }
280            }
281            Ok(false)
282        }
283        PredicateKind::Lt
284        | PredicateKind::Le
285        | PredicateKind::Gt
286        | PredicateKind::Ge
287        | PredicateKind::Eq
288        | PredicateKind::Ne => {
289            check_arity(&kind, &args, 2)?;
290            let lhs = eval_rational_node(args[0], pool, bindings)?;
291            let rhs = eval_rational_node(args[1], pool, bindings)?;
292            Ok(match kind {
293                PredicateKind::Lt => lhs < rhs,
294                PredicateKind::Le => lhs <= rhs,
295                PredicateKind::Gt => lhs > rhs,
296                PredicateKind::Ge => lhs >= rhs,
297                PredicateKind::Eq => lhs == rhs,
298                PredicateKind::Ne => lhs != rhs,
299                _ => unreachable!(),
300            })
301        }
302    }
303}
304
305fn eval_f64_node(
306    expr: ExprId,
307    pool: &ExprPool,
308    bindings: &HashMap<ExprId, f64>,
309) -> Result<f64, EvalError> {
310    match pool.get(expr) {
311        ExprData::Integer(n) => Ok(n.0.to_f64()),
312        ExprData::Rational(r) => Ok(r.0.to_f64()),
313        ExprData::Float(f) => Ok(f.inner.to_f64()),
314        ExprData::Symbol { .. } => bindings
315            .get(&expr)
316            .copied()
317            .ok_or(error(UnsupportedReason::UnboundSymbol { symbol: expr })),
318        ExprData::Add(args) => {
319            let mut sum = 0.0;
320            for arg in args {
321                sum += eval_f64_node(arg, pool, bindings)?;
322            }
323            Ok(sum)
324        }
325        ExprData::Mul(args) => {
326            let mut product = 1.0;
327            for arg in args {
328                product *= eval_f64_node(arg, pool, bindings)?;
329            }
330            Ok(product)
331        }
332        ExprData::Pow { base, exp } => {
333            Ok(eval_f64_node(base, pool, bindings)?.powf(eval_f64_node(exp, pool, bindings)?))
334        }
335        ExprData::Func { name, args } if args.len() == 1 => {
336            let arg = eval_f64_node(args[0], pool, bindings)?;
337            match name.as_str() {
338                "sin" => Ok(arg.sin()),
339                "cos" => Ok(arg.cos()),
340                "exp" => Ok(arg.exp()),
341                "log" => Ok(arg.ln()),
342                "sqrt" => Ok(arg.sqrt()),
343                _ => Err(error(UnsupportedReason::UnsupportedFunction {
344                    name: name.clone(),
345                })),
346            }
347        }
348        ExprData::Func { name, .. } => Err(error(UnsupportedReason::UnsupportedFunction {
349            name: name.clone(),
350        })),
351        ExprData::Piecewise { branches, default } => {
352            for (condition, value) in branches {
353                if eval_f64_predicate(condition, pool, bindings)? {
354                    return eval_f64_node(value, pool, bindings);
355                }
356            }
357            eval_f64_node(default, pool, bindings)
358        }
359        ExprData::Predicate { .. } => Ok(eval_f64_predicate(expr, pool, bindings)? as i32 as f64),
360        other => Err(error(UnsupportedReason::UnsupportedExpression {
361            kind: expr_kind(&other),
362        })),
363    }
364}
365
366fn eval_f64_predicate(
367    expr: ExprId,
368    pool: &ExprPool,
369    bindings: &HashMap<ExprId, f64>,
370) -> Result<bool, EvalError> {
371    let ExprData::Predicate { kind, args } = pool.get(expr) else {
372        return Err(error(UnsupportedReason::IndeterminatePredicate));
373    };
374    match kind {
375        PredicateKind::True => check_arity(&kind, &args, 0).map(|_| true),
376        PredicateKind::False => check_arity(&kind, &args, 0).map(|_| false),
377        PredicateKind::Not => Ok(!eval_f64_predicate(
378            predicate_arg(&kind, &args, 0)?,
379            pool,
380            bindings,
381        )?),
382        PredicateKind::And => {
383            for &arg in &args {
384                if !eval_f64_predicate(arg, pool, bindings)? {
385                    return Ok(false);
386                }
387            }
388            Ok(true)
389        }
390        PredicateKind::Or => {
391            for &arg in &args {
392                if eval_f64_predicate(arg, pool, bindings)? {
393                    return Ok(true);
394                }
395            }
396            Ok(false)
397        }
398        PredicateKind::Lt
399        | PredicateKind::Le
400        | PredicateKind::Gt
401        | PredicateKind::Ge
402        | PredicateKind::Eq
403        | PredicateKind::Ne => {
404            check_arity(&kind, &args, 2)?;
405            let lhs = eval_f64_node(args[0], pool, bindings)?;
406            let rhs = eval_f64_node(args[1], pool, bindings)?;
407            Ok(match kind {
408                PredicateKind::Lt => lhs < rhs,
409                PredicateKind::Le => lhs <= rhs,
410                PredicateKind::Gt => lhs > rhs,
411                PredicateKind::Ge => lhs >= rhs,
412                PredicateKind::Eq => lhs == rhs,
413                PredicateKind::Ne => lhs != rhs,
414                _ => unreachable!(),
415            })
416        }
417    }
418}
419
420pub(crate) fn check_arity(
421    kind: &PredicateKind,
422    args: &[ExprId],
423    expected: usize,
424) -> Result<(), EvalError> {
425    if args.len() == expected {
426        Ok(())
427    } else {
428        Err(error(UnsupportedReason::InvalidPredicateArity {
429            kind: kind.clone(),
430            expected,
431            actual: args.len(),
432        }))
433    }
434}
435
436pub(crate) fn predicate_arg(
437    kind: &PredicateKind,
438    args: &[ExprId],
439    index: usize,
440) -> Result<ExprId, EvalError> {
441    check_arity(kind, args, 1)?;
442    Ok(args[index])
443}
444
445pub(crate) fn expr_kind(expr: &ExprData) -> &'static str {
446    match expr {
447        ExprData::Forall { .. } => "Forall",
448        ExprData::Exists { .. } => "Exists",
449        ExprData::BigO(_) => "BigO",
450        ExprData::RootSum { .. } => "RootSum",
451        _ => "unknown",
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::ball::ArbBall;
459    use crate::kernel::Domain;
460
461    #[test]
462    fn exact_rational_mode_preserves_fractional_result() {
463        let pool = ExprPool::new();
464        let x = pool.symbol("x", Domain::Real);
465        let expr = pool.add(vec![pool.rational(1, 3), x]);
466        let bindings = HashMap::from([(x, Rational::from((1, 6)))]);
467
468        assert_eq!(
469            eval_exact_rational(expr, &pool, &bindings).unwrap(),
470            Rational::from((1, 2))
471        );
472    }
473
474    #[test]
475    fn f64_mode_evaluates_transcendental_function() {
476        let pool = ExprPool::new();
477        let expr = pool.func("sqrt", vec![pool.integer(9_i32)]);
478
479        let result = eval_f64(expr, &pool, &HashMap::new()).unwrap();
480        assert_eq!(result, 3.0);
481    }
482
483    #[test]
484    fn exact_mode_rejects_float_literal_structurally() {
485        let pool = ExprPool::new();
486        let expr = pool.float(0.5, 53);
487
488        assert_eq!(
489            eval_exact_rational(expr, &pool, &HashMap::new())
490                .unwrap_err()
491                .reason,
492            UnsupportedReason::FloatLiteralInExactMode
493        );
494    }
495
496    #[test]
497    fn facade_interval_mode_refuses_threshold_spanning_piecewise() {
498        let pool = ExprPool::new();
499        let x = pool.symbol("x", Domain::Real);
500        let expr = pool.piecewise(
501            vec![(pool.pred_ge(x, pool.integer(0_i32)), pool.integer(1_i32))],
502            pool.integer(-1_i32),
503        );
504        let mut interval = IntervalEval::new(128);
505        interval.bind(x, ArbBall::from_midpoint_radius(0.0, 1.0, 128));
506
507        assert_eq!(
508            eval_interval(expr, &pool, &interval).unwrap_err().reason,
509            UnsupportedReason::IntervalEvaluationFailed
510        );
511    }
512}