Skip to main content

xlog_logic/
arithmetic_eval.rs

1//! Stack-safe scalar evaluation for normalized arithmetic AST expressions.
2
3use std::collections::HashMap;
4
5use xlog_core::{symbol, Result, ScalarType, XlogError};
6use xlog_ir::ConstValue;
7
8use crate::ast::{ArithExpr, CompOp, Term};
9use crate::lower::term_to_typed_const_value;
10
11/// A typed scalar accepted by the arithmetic expression evaluator.
12#[derive(Debug, Clone, PartialEq)]
13pub enum ArithmeticValue {
14    /// Signed 32-bit integer.
15    I32(i32),
16    /// Signed 64-bit integer.
17    I64(i64),
18    /// Unsigned 32-bit integer.
19    U32(u32),
20    /// Unsigned 64-bit integer.
21    U64(u64),
22    /// 32-bit floating-point value.
23    F32(f32),
24    /// 64-bit floating-point value.
25    F64(f64),
26    /// Boolean value.
27    Bool(bool),
28    /// Interned symbol identifier.
29    Symbol(u32),
30    /// String value.
31    String(String),
32}
33
34impl ArithmeticValue {
35    /// Convert an AST term into a scalar value.
36    ///
37    /// Integer and float terms use the AST literal types (`i64` and `f64`). Callers
38    /// with schema information can construct a narrower typed variant directly.
39    pub fn from_term(term: &Term) -> Result<Self> {
40        match term {
41            Term::Integer(value) => Ok(Self::I64(*value)),
42            Term::Float(value) => Ok(Self::F64(*value)),
43            Term::String(value) => Ok(Self::String(value.clone())),
44            Term::Symbol(value) => Ok(Self::Symbol(*value)),
45            Term::Variable(name) => Err(XlogError::Compilation(format!(
46                "Unbound variable {name} in arithmetic"
47            ))),
48            Term::Anonymous => Err(XlogError::Compilation(
49                "Anonymous variable not allowed in arithmetic".to_string(),
50            )),
51            Term::List(_)
52            | Term::Cons { .. }
53            | Term::Compound { .. }
54            | Term::PredRef(_)
55            | Term::Aggregate(_) => Err(XlogError::Compilation(
56                "Arithmetic requires a scalar value".to_string(),
57            )),
58        }
59    }
60
61    /// Convert the evaluated scalar back into an AST term.
62    pub fn into_term(self) -> Result<Term> {
63        Ok(match self {
64            Self::I32(value) => Term::Integer(i64::from(value)),
65            Self::I64(value) => Term::Integer(value),
66            Self::U32(value) => Term::Integer(i64::from(value)),
67            Self::U64(value) => Term::Integer(i64::try_from(value).map_err(|_| {
68                XlogError::Compilation("u64 arithmetic result exceeds AST integer range".into())
69            })?),
70            Self::F32(value) => Term::Float(f64::from(value)),
71            Self::F64(value) => Term::Float(value),
72            Self::Bool(value) => Term::Integer(i64::from(value)),
73            Self::Symbol(value) => Term::Symbol(value),
74            Self::String(value) => Term::String(value),
75        })
76    }
77
78    fn kind(&self) -> ArithmeticValueKind {
79        match self {
80            Self::I32(_) => ArithmeticValueKind::I32,
81            Self::I64(_) => ArithmeticValueKind::I64,
82            Self::U32(_) => ArithmeticValueKind::U32,
83            Self::U64(_) => ArithmeticValueKind::U64,
84            Self::F32(_) => ArithmeticValueKind::F32,
85            Self::F64(_) => ArithmeticValueKind::F64,
86            Self::Bool(_) => ArithmeticValueKind::Bool,
87            Self::Symbol(_) => ArithmeticValueKind::Symbol,
88            Self::String(_) => ArithmeticValueKind::String,
89        }
90    }
91
92    /// Return the runtime scalar type represented by this value.
93    ///
94    /// Source-only strings have no runtime scalar type until they are interned as
95    /// symbols by [`ArithmeticValue::from_typed_term`].
96    pub fn scalar_type(&self) -> Option<ScalarType> {
97        match self.kind() {
98            ArithmeticValueKind::I32 => Some(ScalarType::I32),
99            ArithmeticValueKind::I64 => Some(ScalarType::I64),
100            ArithmeticValueKind::U32 => Some(ScalarType::U32),
101            ArithmeticValueKind::U64 => Some(ScalarType::U64),
102            ArithmeticValueKind::F32 => Some(ScalarType::F32),
103            ArithmeticValueKind::F64 => Some(ScalarType::F64),
104            ArithmeticValueKind::Bool => Some(ScalarType::Bool),
105            ArithmeticValueKind::Symbol => Some(ScalarType::Symbol),
106            ArithmeticValueKind::String => None,
107        }
108    }
109
110    /// Convert an AST scalar term using the same declared-type conversion as lowering.
111    pub fn from_typed_term(term: &Term, expected: ScalarType) -> Result<Self> {
112        let value = term_to_typed_const_value(term, expected)?.ok_or_else(|| {
113            XlogError::Compilation("Arithmetic requires a bound scalar value".to_string())
114        })?;
115        Ok(match value {
116            ConstValue::I32(value) => Self::I32(value),
117            ConstValue::I64(value) => Self::I64(value),
118            ConstValue::U32(value) => Self::U32(value),
119            ConstValue::U64(value) => Self::U64(value),
120            ConstValue::F32(value) => Self::F32(value),
121            ConstValue::F64(value) => Self::F64(value),
122            ConstValue::Bool(value) => Self::Bool(value),
123            ConstValue::Symbol(value) => Self::Symbol(symbol::intern(&value)),
124        })
125    }
126
127    fn as_f64(&self) -> Result<f64> {
128        match self {
129            Self::I32(value) => Ok(f64::from(*value)),
130            Self::I64(value) => Ok(*value as f64),
131            Self::U32(value) => Ok(f64::from(*value)),
132            Self::U64(value) => Ok(*value as f64),
133            Self::F32(value) => Ok(f64::from(*value)),
134            Self::F64(value) => Ok(*value),
135            Self::Bool(value) => Ok(if *value { 1.0 } else { 0.0 }),
136            Self::Symbol(value) => Ok(f64::from(*value)),
137            Self::String(_) => Err(numeric_type_error()),
138        }
139    }
140
141    fn cast(self, target: ScalarType) -> Result<Self> {
142        if self.scalar_type() == Some(target) {
143            return Ok(self);
144        }
145        match target {
146            ScalarType::I32 => Ok(Self::I32(self.cast_i64()? as i32)),
147            ScalarType::I64 => Ok(Self::I64(self.cast_i64()?)),
148            ScalarType::U32 => Ok(Self::U32(self.cast_i64()? as u32)),
149            ScalarType::U64 => Ok(Self::U64(self.cast_i64()? as u64)),
150            ScalarType::F32 => Ok(Self::F32(self.as_f64()? as f32)),
151            ScalarType::F64 => Ok(Self::F64(self.as_f64()?)),
152            ScalarType::Bool => Ok(Self::Bool(self.cast_i64()? != 0)),
153            ScalarType::Symbol => Ok(Self::Symbol(self.cast_i64()? as u32)),
154        }
155    }
156
157    fn cast_i64(&self) -> Result<i64> {
158        match self {
159            Self::I32(value) => Ok(i64::from(*value)),
160            Self::I64(value) => Ok(*value),
161            Self::U32(value) => Ok(i64::from(*value)),
162            Self::U64(value) => Ok(*value as i64),
163            Self::F32(value) => float_to_i64(f64::from(*value)),
164            Self::F64(value) => float_to_i64(*value),
165            Self::Bool(value) => Ok(if *value { 1 } else { 0 }),
166            Self::Symbol(value) => Ok(i64::from(*value)),
167            Self::String(_) => Err(numeric_type_error()),
168        }
169    }
170}
171
172#[derive(Clone, Copy, PartialEq, Eq)]
173enum ArithmeticValueKind {
174    I32,
175    I64,
176    U32,
177    U64,
178    F32,
179    F64,
180    Bool,
181    Symbol,
182    String,
183}
184
185#[derive(Clone, Copy)]
186enum BinaryOperation {
187    Add,
188    Subtract,
189    Multiply,
190    Divide,
191    Modulo,
192    Minimum,
193    Maximum,
194    Power,
195}
196
197enum EvaluationTask<'a> {
198    Expression(&'a ArithExpr),
199    FinishBinary(BinaryOperation),
200    FinishAbsoluteValue,
201    FinishCast(ScalarType),
202    FinishComparison(CompOp),
203    FinishConditional,
204}
205
206enum EvaluationValue {
207    Arithmetic(ArithmeticValue),
208    Predicate(bool),
209}
210
211/// Evaluate a normalized arithmetic expression without recursive AST traversal.
212///
213/// Binary operations require matching numeric types. Integer operations wrap on
214/// overflow. Integer division by zero returns the maximum value of the operand type,
215/// integer remainder by zero returns zero, and floating-point zero division follows
216/// IEEE 754 with canonical positive NaNs. `pow` always returns `f64`. User-defined
217/// calls must be expanded before this function is called.
218pub fn evaluate_arithmetic_expression(
219    expression: &ArithExpr,
220    bindings: &HashMap<String, ArithmeticValue>,
221) -> Result<ArithmeticValue> {
222    let mut tasks = vec![EvaluationTask::Expression(expression)];
223    let mut values = Vec::new();
224
225    while let Some(task) = tasks.pop() {
226        match task {
227            EvaluationTask::Expression(expression) => match expression {
228                ArithExpr::Variable(name) => values.push(EvaluationValue::Arithmetic(
229                    bindings.get(name).cloned().ok_or_else(|| {
230                        XlogError::Compilation(format!("Unbound variable {name} in arithmetic"))
231                    })?,
232                )),
233                ArithExpr::Integer(value) => {
234                    values.push(EvaluationValue::Arithmetic(ArithmeticValue::I64(*value)));
235                }
236                ArithExpr::Float(value) => {
237                    values.push(EvaluationValue::Arithmetic(ArithmeticValue::F64(*value)));
238                }
239                ArithExpr::Add(left, right) => {
240                    schedule_binary(&mut tasks, left, right, BinaryOperation::Add)
241                }
242                ArithExpr::Sub(left, right) => {
243                    schedule_binary(&mut tasks, left, right, BinaryOperation::Subtract)
244                }
245                ArithExpr::Mul(left, right) => {
246                    schedule_binary(&mut tasks, left, right, BinaryOperation::Multiply)
247                }
248                ArithExpr::Div(left, right) => {
249                    schedule_binary(&mut tasks, left, right, BinaryOperation::Divide)
250                }
251                ArithExpr::Mod(left, right) => {
252                    schedule_binary(&mut tasks, left, right, BinaryOperation::Modulo)
253                }
254                ArithExpr::Min(left, right) => {
255                    schedule_binary(&mut tasks, left, right, BinaryOperation::Minimum)
256                }
257                ArithExpr::Max(left, right) => {
258                    schedule_binary(&mut tasks, left, right, BinaryOperation::Maximum)
259                }
260                ArithExpr::Pow(left, right) => {
261                    schedule_binary(&mut tasks, left, right, BinaryOperation::Power)
262                }
263                ArithExpr::Abs(inner) => {
264                    tasks.push(EvaluationTask::FinishAbsoluteValue);
265                    tasks.push(EvaluationTask::Expression(inner));
266                }
267                ArithExpr::Cast(inner, target) => {
268                    tasks.push(EvaluationTask::FinishCast(*target));
269                    tasks.push(EvaluationTask::Expression(inner));
270                }
271                ArithExpr::Conditional {
272                    cond_left,
273                    cond_op,
274                    cond_right,
275                    then_expr,
276                    else_expr,
277                } => {
278                    tasks.push(EvaluationTask::FinishConditional);
279                    tasks.push(EvaluationTask::Expression(else_expr));
280                    tasks.push(EvaluationTask::Expression(then_expr));
281                    tasks.push(EvaluationTask::FinishComparison(*cond_op));
282                    tasks.push(EvaluationTask::Expression(cond_right));
283                    tasks.push(EvaluationTask::Expression(cond_left));
284                }
285                ArithExpr::FuncCall { name, .. } => {
286                    return Err(XlogError::Compilation(format!(
287                        "Function call `{name}` must be expanded before arithmetic evaluation"
288                    )));
289                }
290            },
291            EvaluationTask::FinishBinary(operation) => {
292                let right = pop_arithmetic(&mut values)?;
293                let left = pop_arithmetic(&mut values)?;
294                values.push(EvaluationValue::Arithmetic(evaluate_binary(
295                    operation, left, right,
296                )?));
297            }
298            EvaluationTask::FinishAbsoluteValue => {
299                let value = pop_arithmetic(&mut values)?;
300                values.push(EvaluationValue::Arithmetic(evaluate_abs(value)?));
301            }
302            EvaluationTask::FinishCast(target) => {
303                let value = pop_arithmetic(&mut values)?;
304                values.push(EvaluationValue::Arithmetic(value.cast(target)?));
305            }
306            EvaluationTask::FinishComparison(operator) => {
307                let right = pop_arithmetic(&mut values)?;
308                let left = pop_arithmetic(&mut values)?;
309                values.push(EvaluationValue::Predicate(compare_arithmetic_values(
310                    &left, operator, &right,
311                )?));
312            }
313            EvaluationTask::FinishConditional => {
314                let else_value = pop_arithmetic(&mut values)?;
315                let then_value = pop_arithmetic(&mut values)?;
316                let condition = pop_predicate(&mut values)?;
317                if then_value.kind() != else_value.kind() {
318                    return Err(XlogError::Compilation(
319                        "Conditional branches require matching scalar types".to_string(),
320                    ));
321                }
322                values.push(EvaluationValue::Arithmetic(if condition {
323                    then_value
324                } else {
325                    else_value
326                }));
327            }
328        }
329    }
330
331    let result = pop_arithmetic(&mut values)?;
332    if values.is_empty() {
333        Ok(result)
334    } else {
335        Err(evaluation_state_error())
336    }
337}
338
339fn schedule_binary<'a>(
340    tasks: &mut Vec<EvaluationTask<'a>>,
341    left: &'a ArithExpr,
342    right: &'a ArithExpr,
343    operation: BinaryOperation,
344) {
345    tasks.push(EvaluationTask::FinishBinary(operation));
346    tasks.push(EvaluationTask::Expression(right));
347    tasks.push(EvaluationTask::Expression(left));
348}
349
350fn pop_arithmetic(values: &mut Vec<EvaluationValue>) -> Result<ArithmeticValue> {
351    match values.pop() {
352        Some(EvaluationValue::Arithmetic(value)) => Ok(value),
353        Some(EvaluationValue::Predicate(_)) | None => Err(evaluation_state_error()),
354    }
355}
356
357fn pop_predicate(values: &mut Vec<EvaluationValue>) -> Result<bool> {
358    match values.pop() {
359        Some(EvaluationValue::Predicate(value)) => Ok(value),
360        Some(EvaluationValue::Arithmetic(_)) | None => Err(evaluation_state_error()),
361    }
362}
363
364fn evaluate_binary(
365    operation: BinaryOperation,
366    left: ArithmeticValue,
367    right: ArithmeticValue,
368) -> Result<ArithmeticValue> {
369    if matches!(operation, BinaryOperation::Power) {
370        return Ok(ArithmeticValue::F64(normalize_nan_f64(
371            left.as_f64()?.powf(right.as_f64()?),
372        )));
373    }
374    if left.kind() != right.kind() {
375        return Err(XlogError::Compilation(
376            "Arithmetic operation requires matching numeric types".to_string(),
377        ));
378    }
379
380    macro_rules! integer_operation {
381        ($left:expr, $right:expr, $variant:ident, $type:ty) => {{
382            let value = match operation {
383                BinaryOperation::Add => $left.wrapping_add($right),
384                BinaryOperation::Subtract => $left.wrapping_sub($right),
385                BinaryOperation::Multiply => $left.wrapping_mul($right),
386                BinaryOperation::Divide if $right == 0 => <$type>::MAX,
387                BinaryOperation::Divide => $left.wrapping_div($right),
388                BinaryOperation::Modulo if $right == 0 => 0,
389                BinaryOperation::Modulo => $left.wrapping_rem($right),
390                BinaryOperation::Minimum => $left.min($right),
391                BinaryOperation::Maximum => $left.max($right),
392                BinaryOperation::Power => unreachable!("power handled above"),
393            };
394            ArithmeticValue::$variant(value)
395        }};
396    }
397
398    let value = match (left, right) {
399        (ArithmeticValue::I32(left), ArithmeticValue::I32(right)) => {
400            integer_operation!(left, right, I32, i32)
401        }
402        (ArithmeticValue::I64(left), ArithmeticValue::I64(right)) => {
403            integer_operation!(left, right, I64, i64)
404        }
405        (ArithmeticValue::U32(left), ArithmeticValue::U32(right)) => {
406            integer_operation!(left, right, U32, u32)
407        }
408        (ArithmeticValue::U64(left), ArithmeticValue::U64(right)) => {
409            integer_operation!(left, right, U64, u64)
410        }
411        (ArithmeticValue::F32(left), ArithmeticValue::F32(right)) => {
412            ArithmeticValue::F32(match operation {
413                BinaryOperation::Add => left + right,
414                BinaryOperation::Subtract => left - right,
415                BinaryOperation::Multiply => left * right,
416                BinaryOperation::Divide => normalize_nan_f32(left / right),
417                BinaryOperation::Modulo => normalize_nan_f32(left % right),
418                BinaryOperation::Minimum => {
419                    if left < right {
420                        left
421                    } else {
422                        right
423                    }
424                }
425                BinaryOperation::Maximum => {
426                    if left > right {
427                        left
428                    } else {
429                        right
430                    }
431                }
432                BinaryOperation::Power => unreachable!("power handled above"),
433            })
434        }
435        (ArithmeticValue::F64(left), ArithmeticValue::F64(right)) => {
436            ArithmeticValue::F64(match operation {
437                BinaryOperation::Add => left + right,
438                BinaryOperation::Subtract => left - right,
439                BinaryOperation::Multiply => left * right,
440                BinaryOperation::Divide => normalize_nan_f64(left / right),
441                BinaryOperation::Modulo => normalize_nan_f64(left % right),
442                BinaryOperation::Minimum => {
443                    if left < right {
444                        left
445                    } else {
446                        right
447                    }
448                }
449                BinaryOperation::Maximum => {
450                    if left > right {
451                        left
452                    } else {
453                        right
454                    }
455                }
456                BinaryOperation::Power => unreachable!("power handled above"),
457            })
458        }
459        _ => return Err(numeric_type_error()),
460    };
461    Ok(value)
462}
463
464fn evaluate_abs(value: ArithmeticValue) -> Result<ArithmeticValue> {
465    match value {
466        ArithmeticValue::I32(value) => Ok(ArithmeticValue::I32(value.wrapping_abs())),
467        ArithmeticValue::I64(value) => Ok(ArithmeticValue::I64(value.wrapping_abs())),
468        ArithmeticValue::U32(value) => Ok(ArithmeticValue::U32(value)),
469        ArithmeticValue::U64(value) => Ok(ArithmeticValue::U64(value)),
470        ArithmeticValue::F32(value) => Ok(ArithmeticValue::F32(value.abs())),
471        ArithmeticValue::F64(value) => Ok(ArithmeticValue::F64(value.abs())),
472        ArithmeticValue::Bool(_) | ArithmeticValue::Symbol(_) | ArithmeticValue::String(_) => Err(
473            XlogError::Compilation("abs() requires numeric input".to_string()),
474        ),
475    }
476}
477
478/// Compare two scalar values using the execution runtime's comparison semantics.
479///
480/// Equal-width floating-point operands retain their width. When exactly one operand
481/// is floating point, or the widths differ, both numeric values are converted to
482/// `f64`, matching runtime predicate evaluation. Float equality remains IEEE 754;
483/// ordered comparisons use IEEE total ordering.
484pub fn compare_arithmetic_values(
485    left: &ArithmeticValue,
486    op: CompOp,
487    right: &ArithmeticValue,
488) -> Result<bool> {
489    let mixed_float_numeric = left.kind() != right.kind()
490        && (is_float(left) || is_float(right))
491        && is_numeric(left)
492        && is_numeric(right);
493    if mixed_float_numeric {
494        return Ok(compare_f64(left.as_f64()?, op, right.as_f64()?));
495    }
496    if left.kind() != right.kind() {
497        return Err(XlogError::Compilation(
498            "Comparison between differing types is not supported".to_string(),
499        ));
500    }
501    macro_rules! compare_ordered {
502        ($left:expr, $right:expr) => {
503            match op {
504                CompOp::Eq => $left == $right,
505                CompOp::Ne => $left != $right,
506                CompOp::Lt => $left < $right,
507                CompOp::Le => $left <= $right,
508                CompOp::Gt => $left > $right,
509                CompOp::Ge => $left >= $right,
510            }
511        };
512    }
513    Ok(match (left, right) {
514        (ArithmeticValue::I32(left), ArithmeticValue::I32(right)) => {
515            compare_ordered!(left, right)
516        }
517        (ArithmeticValue::I64(left), ArithmeticValue::I64(right)) => {
518            compare_ordered!(left, right)
519        }
520        (ArithmeticValue::U32(left), ArithmeticValue::U32(right)) => {
521            compare_ordered!(left, right)
522        }
523        (ArithmeticValue::U64(left), ArithmeticValue::U64(right)) => {
524            compare_ordered!(left, right)
525        }
526        (ArithmeticValue::F32(left), ArithmeticValue::F32(right)) => compare_f32(*left, op, *right),
527        (ArithmeticValue::F64(left), ArithmeticValue::F64(right)) => compare_f64(*left, op, *right),
528        (ArithmeticValue::Bool(left), ArithmeticValue::Bool(right)) => {
529            compare_ordered!(left, right)
530        }
531        (ArithmeticValue::Symbol(left), ArithmeticValue::Symbol(right)) => {
532            compare_ordered!(left, right)
533        }
534        (ArithmeticValue::String(left), ArithmeticValue::String(right)) => {
535            compare_ordered!(left, right)
536        }
537        _ => unreachable!("scalar types checked above"),
538    })
539}
540
541fn compare_f32(left: f32, op: CompOp, right: f32) -> bool {
542    match op {
543        CompOp::Eq => left == right,
544        CompOp::Ne => left != right,
545        CompOp::Lt => left.total_cmp(&right).is_lt(),
546        CompOp::Le => !left.total_cmp(&right).is_gt(),
547        CompOp::Gt => left.total_cmp(&right).is_gt(),
548        CompOp::Ge => !left.total_cmp(&right).is_lt(),
549    }
550}
551
552fn compare_f64(left: f64, op: CompOp, right: f64) -> bool {
553    match op {
554        CompOp::Eq => left == right,
555        CompOp::Ne => left != right,
556        CompOp::Lt => left.total_cmp(&right).is_lt(),
557        CompOp::Le => !left.total_cmp(&right).is_gt(),
558        CompOp::Gt => left.total_cmp(&right).is_gt(),
559        CompOp::Ge => !left.total_cmp(&right).is_lt(),
560    }
561}
562
563fn is_float(value: &ArithmeticValue) -> bool {
564    matches!(value, ArithmeticValue::F32(_) | ArithmeticValue::F64(_))
565}
566
567fn is_numeric(value: &ArithmeticValue) -> bool {
568    matches!(
569        value,
570        ArithmeticValue::I32(_)
571            | ArithmeticValue::I64(_)
572            | ArithmeticValue::U32(_)
573            | ArithmeticValue::U64(_)
574            | ArithmeticValue::F32(_)
575            | ArithmeticValue::F64(_)
576    )
577}
578
579fn normalize_nan_f32(value: f32) -> f32 {
580    if value.is_nan() {
581        f32::from_bits(0x7fc0_0000)
582    } else {
583        value
584    }
585}
586
587fn normalize_nan_f64(value: f64) -> f64 {
588    if value.is_nan() {
589        f64::from_bits(0x7ff8_0000_0000_0000)
590    } else {
591        value
592    }
593}
594
595fn float_to_i64(value: f64) -> Result<i64> {
596    const I64_MIN_AS_F64: f64 = -9_223_372_036_854_775_808.0;
597    const I64_MAX_EXCLUSIVE_AS_F64: f64 = 9_223_372_036_854_775_808.0;
598    if !value.is_finite() || !(I64_MIN_AS_F64..I64_MAX_EXCLUSIVE_AS_F64).contains(&value.trunc()) {
599        return Err(XlogError::Compilation(
600            "Floating-point value cannot be represented by the runtime integer cast".to_string(),
601        ));
602    }
603    Ok(value as i64)
604}
605
606fn numeric_type_error() -> XlogError {
607    XlogError::Compilation("Arithmetic operation requires matching numeric types".to_string())
608}
609
610fn evaluation_state_error() -> XlogError {
611    XlogError::Compilation("Invalid arithmetic evaluation state".to_string())
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    fn evaluate(expression: &ArithExpr) -> Result<ArithmeticValue> {
619        evaluate_arithmetic_expression(expression, &HashMap::new())
620    }
621
622    #[test]
623    fn integer_edge_cases_match_runtime_contract() {
624        let zero = ArithExpr::Integer(0);
625        let one = ArithExpr::Integer(1);
626        assert_eq!(
627            evaluate(&ArithExpr::Div(
628                Box::new(one.clone()),
629                Box::new(zero.clone())
630            ))
631            .expect("division result"),
632            ArithmeticValue::I64(i64::MAX)
633        );
634        assert_eq!(
635            evaluate(&ArithExpr::Mod(Box::new(one), Box::new(zero))).expect("remainder result"),
636            ArithmeticValue::I64(0)
637        );
638        assert_eq!(
639            evaluate(&ArithExpr::Add(
640                Box::new(ArithExpr::Integer(i64::MAX)),
641                Box::new(ArithExpr::Integer(1)),
642            ))
643            .expect("wrapping sum"),
644            ArithmeticValue::I64(i64::MIN)
645        );
646
647        let bindings = HashMap::from([
648            (
649                "wide_unsigned".to_string(),
650                ArithmeticValue::U32(2_147_483_648),
651            ),
652            ("max_signed".to_string(), ArithmeticValue::I32(i32::MAX)),
653        ]);
654        assert_eq!(
655            evaluate_arithmetic_expression(
656                &ArithExpr::Add(
657                    Box::new(ArithExpr::Variable("wide_unsigned".to_string())),
658                    Box::new(ArithExpr::Variable("wide_unsigned".to_string())),
659                ),
660                &bindings,
661            )
662            .expect("u32 wrapping sum"),
663            ArithmeticValue::U32(0)
664        );
665        assert_eq!(
666            evaluate_arithmetic_expression(
667                &ArithExpr::Add(
668                    Box::new(ArithExpr::Variable("max_signed".to_string())),
669                    Box::new(ArithExpr::Cast(
670                        Box::new(ArithExpr::Integer(1)),
671                        ScalarType::I32,
672                    )),
673                ),
674                &bindings,
675            )
676            .expect("i32 wrapping sum"),
677            ArithmeticValue::I32(i32::MIN)
678        );
679    }
680
681    #[test]
682    fn casts_power_and_conditionals_preserve_types_and_order() {
683        let expression = ArithExpr::Conditional {
684            cond_left: Box::new(ArithExpr::Integer(1)),
685            cond_op: CompOp::Eq,
686            cond_right: Box::new(ArithExpr::Integer(1)),
687            then_expr: Box::new(ArithExpr::Cast(
688                Box::new(ArithExpr::Pow(
689                    Box::new(ArithExpr::Integer(2)),
690                    Box::new(ArithExpr::Integer(3)),
691                )),
692                ScalarType::F32,
693            )),
694            else_expr: Box::new(ArithExpr::Cast(
695                Box::new(ArithExpr::Integer(0)),
696                ScalarType::F32,
697            )),
698        };
699        assert_eq!(
700            evaluate(&expression).expect("conditional result"),
701            ArithmeticValue::F32(8.0)
702        );
703
704        let eager_error = ArithExpr::Conditional {
705            cond_left: Box::new(ArithExpr::Integer(1)),
706            cond_op: CompOp::Eq,
707            cond_right: Box::new(ArithExpr::Integer(1)),
708            then_expr: Box::new(ArithExpr::Variable("then_missing".to_string())),
709            else_expr: Box::new(ArithExpr::Variable("else_missing".to_string())),
710        };
711        let error = evaluate(&eager_error).expect_err("eager branch error");
712        assert!(error.to_string().contains("then_missing"), "{error}");
713
714        let bindings = HashMap::from([
715            ("enabled".to_string(), ArithmeticValue::Bool(true)),
716            ("label".to_string(), ArithmeticValue::Symbol(7)),
717        ]);
718        for (name, expected) in [("enabled", 1.0), ("label", 7.0)] {
719            let cast = ArithExpr::Cast(
720                Box::new(ArithExpr::Variable(name.to_string())),
721                ScalarType::F64,
722            );
723            assert_eq!(
724                evaluate_arithmetic_expression(&cast, &bindings).expect("runtime-compatible cast"),
725                ArithmeticValue::F64(expected)
726            );
727        }
728    }
729
730    #[test]
731    fn strings_and_symbols_remain_distinct_without_panicking() {
732        let mut bindings = HashMap::new();
733        bindings.insert(
734            "string_value".to_string(),
735            ArithmeticValue::String("value".to_string()),
736        );
737        bindings.insert("symbol_value".to_string(), ArithmeticValue::Symbol(7));
738        let comparison = ArithExpr::Conditional {
739            cond_left: Box::new(ArithExpr::Variable("string_value".to_string())),
740            cond_op: CompOp::Eq,
741            cond_right: Box::new(ArithExpr::Variable("symbol_value".to_string())),
742            then_expr: Box::new(ArithExpr::Integer(1)),
743            else_expr: Box::new(ArithExpr::Integer(0)),
744        };
745        let error = evaluate_arithmetic_expression(&comparison, &bindings)
746            .expect_err("string/symbol comparison must fail");
747        assert!(error.to_string().contains("differing types"), "{error}");
748
749        let cast = ArithExpr::Cast(
750            Box::new(ArithExpr::Variable("string_value".to_string())),
751            ScalarType::Symbol,
752        );
753        assert!(evaluate_arithmetic_expression(&cast, &bindings).is_err());
754    }
755
756    #[test]
757    fn float_operations_match_cuda_nan_and_selection_semantics() {
758        let nan = f64::from_bits(0xfff8_0000_0000_0042);
759        let mut bindings = HashMap::new();
760        bindings.insert("nan".to_string(), ArithmeticValue::F64(nan));
761        bindings.insert("one".to_string(), ArithmeticValue::F64(1.0));
762        bindings.insert("positive_zero".to_string(), ArithmeticValue::F64(0.0));
763        bindings.insert("negative_zero".to_string(), ArithmeticValue::F64(-0.0));
764
765        for expression in [
766            ArithExpr::Div(
767                Box::new(ArithExpr::Float(0.0)),
768                Box::new(ArithExpr::Float(0.0)),
769            ),
770            ArithExpr::Mod(
771                Box::new(ArithExpr::Float(0.0)),
772                Box::new(ArithExpr::Float(0.0)),
773            ),
774            ArithExpr::Pow(
775                Box::new(ArithExpr::Float(-1.0)),
776                Box::new(ArithExpr::Float(0.5)),
777            ),
778        ] {
779            let ArithmeticValue::F64(value) =
780                evaluate_arithmetic_expression(&expression, &bindings).expect("float result")
781            else {
782                panic!("expected f64 result");
783            };
784            assert_eq!(value.to_bits(), 0x7ff8_0000_0000_0000);
785        }
786
787        assert_eq!(
788            evaluate_arithmetic_expression(
789                &ArithExpr::Min(
790                    Box::new(ArithExpr::Variable("nan".to_string())),
791                    Box::new(ArithExpr::Variable("one".to_string())),
792                ),
793                &bindings,
794            )
795            .expect("minimum"),
796            ArithmeticValue::F64(1.0)
797        );
798        let ArithmeticValue::F64(minimum_zero) = evaluate_arithmetic_expression(
799            &ArithExpr::Min(
800                Box::new(ArithExpr::Variable("negative_zero".to_string())),
801                Box::new(ArithExpr::Variable("positive_zero".to_string())),
802            ),
803            &bindings,
804        )
805        .expect("minimum zero") else {
806            panic!("expected f64 minimum");
807        };
808        assert_eq!(minimum_zero.to_bits(), 0.0_f64.to_bits());
809        let ArithmeticValue::F64(maximum_zero) = evaluate_arithmetic_expression(
810            &ArithExpr::Max(
811                Box::new(ArithExpr::Variable("positive_zero".to_string())),
812                Box::new(ArithExpr::Variable("negative_zero".to_string())),
813            ),
814            &bindings,
815        )
816        .expect("maximum zero") else {
817            panic!("expected f64 maximum");
818        };
819        assert_eq!(maximum_zero.to_bits(), (-0.0_f64).to_bits());
820    }
821
822    #[test]
823    fn float_comparisons_use_runtime_promotion_and_total_ordering() {
824        let positive_nan = ArithmeticValue::F64(f64::from_bits(0x7ff8_0000_0000_0000));
825        assert!(compare_arithmetic_values(
826            &positive_nan,
827            CompOp::Gt,
828            &ArithmeticValue::F64(f64::INFINITY)
829        )
830        .expect("NaN ordering"));
831        assert!(
832            !compare_arithmetic_values(&positive_nan, CompOp::Eq, &positive_nan)
833                .expect("NaN equality")
834        );
835        assert!(
836            compare_arithmetic_values(&positive_nan, CompOp::Ne, &positive_nan)
837                .expect("NaN inequality")
838        );
839        assert!(compare_arithmetic_values(
840            &ArithmeticValue::F64(-0.0),
841            CompOp::Lt,
842            &ArithmeticValue::F64(0.0)
843        )
844        .expect("signed zero ordering"));
845        assert!(compare_arithmetic_values(
846            &ArithmeticValue::F32(2.5),
847            CompOp::Gt,
848            &ArithmeticValue::I64(2)
849        )
850        .expect("mixed numeric comparison"));
851
852        let conditional = ArithExpr::Conditional {
853            cond_left: Box::new(ArithExpr::Variable("float".to_string())),
854            cond_op: CompOp::Lt,
855            cond_right: Box::new(ArithExpr::Variable("integer".to_string())),
856            then_expr: Box::new(ArithExpr::Integer(1)),
857            else_expr: Box::new(ArithExpr::Integer(0)),
858        };
859        let bindings = HashMap::from([
860            ("float".to_string(), ArithmeticValue::F32(2.5)),
861            ("integer".to_string(), ArithmeticValue::I64(2)),
862        ]);
863        assert_eq!(
864            evaluate_arithmetic_expression(&conditional, &bindings).expect("conditional"),
865            ArithmeticValue::I64(0)
866        );
867
868        let mismatched_branches = ArithExpr::Conditional {
869            cond_left: Box::new(ArithExpr::Integer(1)),
870            cond_op: CompOp::Eq,
871            cond_right: Box::new(ArithExpr::Integer(1)),
872            then_expr: Box::new(ArithExpr::Integer(1)),
873            else_expr: Box::new(ArithExpr::Float(1.0)),
874        };
875        assert!(evaluate(&mismatched_branches).is_err());
876    }
877
878    #[test]
879    fn typed_terms_follow_lowering_widths_and_reject_undefined_float_casts() {
880        assert_eq!(
881            ArithmeticValue::from_typed_term(&Term::Integer(2_147_483_648), ScalarType::U32)
882                .expect("u32 literal"),
883            ArithmeticValue::U32(2_147_483_648)
884        );
885        assert_eq!(
886            ArithmeticValue::from_typed_term(&Term::Float(1.5), ScalarType::F32)
887                .expect("f32 literal"),
888            ArithmeticValue::F32(1.5)
889        );
890        let invalid_cast = ArithExpr::Cast(Box::new(ArithExpr::Float(f64::NAN)), ScalarType::I64);
891        assert!(evaluate(&invalid_cast).is_err());
892    }
893
894    #[test]
895    fn configured_depth_expression_uses_bounded_native_stack() {
896        let mut expression = ArithExpr::Integer(0);
897        for _ in 0..1_000 {
898            expression = ArithExpr::Add(Box::new(expression), Box::new(ArithExpr::Integer(1)));
899        }
900        assert_eq!(
901            evaluate(&expression).expect("deep expression result"),
902            ArithmeticValue::I64(1_000)
903        );
904    }
905}