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::{f32_total_order_key, f64_total_order_key, 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 f32_total_order_key(left) <= f32_total_order_key(right) {
420                        left
421                    } else {
422                        right
423                    }
424                }
425                BinaryOperation::Maximum => {
426                    if f32_total_order_key(left) >= f32_total_order_key(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 f64_total_order_key(left) <= f64_total_order_key(right) {
444                        left
445                    } else {
446                        right
447                    }
448                }
449                BinaryOperation::Maximum => {
450                    if f64_total_order_key(left) >= f64_total_order_key(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. Every comparison operator uses
483/// XLOG's IEEE 754 total order, including equality of exact NaN bit patterns and
484/// distinction between negative and positive zero.
485pub fn compare_arithmetic_values(
486    left: &ArithmeticValue,
487    op: CompOp,
488    right: &ArithmeticValue,
489) -> Result<bool> {
490    let mixed_float_numeric = left.kind() != right.kind()
491        && (is_float(left) || is_float(right))
492        && is_numeric(left)
493        && is_numeric(right);
494    if mixed_float_numeric {
495        return Ok(compare_f64(left.as_f64()?, op, right.as_f64()?));
496    }
497    if left.kind() != right.kind() {
498        return Err(XlogError::Compilation(
499            "Comparison between differing types is not supported".to_string(),
500        ));
501    }
502    macro_rules! compare_ordered {
503        ($left:expr, $right:expr) => {
504            match op {
505                CompOp::Eq => $left == $right,
506                CompOp::Ne => $left != $right,
507                CompOp::Lt => $left < $right,
508                CompOp::Le => $left <= $right,
509                CompOp::Gt => $left > $right,
510                CompOp::Ge => $left >= $right,
511            }
512        };
513    }
514    Ok(match (left, right) {
515        (ArithmeticValue::I32(left), ArithmeticValue::I32(right)) => {
516            compare_ordered!(left, right)
517        }
518        (ArithmeticValue::I64(left), ArithmeticValue::I64(right)) => {
519            compare_ordered!(left, right)
520        }
521        (ArithmeticValue::U32(left), ArithmeticValue::U32(right)) => {
522            compare_ordered!(left, right)
523        }
524        (ArithmeticValue::U64(left), ArithmeticValue::U64(right)) => {
525            compare_ordered!(left, right)
526        }
527        (ArithmeticValue::F32(left), ArithmeticValue::F32(right)) => compare_f32(*left, op, *right),
528        (ArithmeticValue::F64(left), ArithmeticValue::F64(right)) => compare_f64(*left, op, *right),
529        (ArithmeticValue::Bool(left), ArithmeticValue::Bool(right)) => {
530            compare_ordered!(left, right)
531        }
532        (ArithmeticValue::Symbol(left), ArithmeticValue::Symbol(right)) => {
533            compare_ordered!(left, right)
534        }
535        (ArithmeticValue::String(left), ArithmeticValue::String(right)) => {
536            compare_ordered!(left, right)
537        }
538        _ => unreachable!("scalar types checked above"),
539    })
540}
541
542fn compare_f32(left: f32, op: CompOp, right: f32) -> bool {
543    let left = f32_total_order_key(left);
544    let right = f32_total_order_key(right);
545    match op {
546        CompOp::Eq => left == right,
547        CompOp::Ne => left != right,
548        CompOp::Lt => left < right,
549        CompOp::Le => left <= right,
550        CompOp::Gt => left > right,
551        CompOp::Ge => left >= right,
552    }
553}
554
555fn compare_f64(left: f64, op: CompOp, right: f64) -> bool {
556    let left = f64_total_order_key(left);
557    let right = f64_total_order_key(right);
558    match op {
559        CompOp::Eq => left == right,
560        CompOp::Ne => left != right,
561        CompOp::Lt => left < right,
562        CompOp::Le => left <= right,
563        CompOp::Gt => left > right,
564        CompOp::Ge => left >= right,
565    }
566}
567
568fn is_float(value: &ArithmeticValue) -> bool {
569    matches!(value, ArithmeticValue::F32(_) | ArithmeticValue::F64(_))
570}
571
572fn is_numeric(value: &ArithmeticValue) -> bool {
573    matches!(
574        value,
575        ArithmeticValue::I32(_)
576            | ArithmeticValue::I64(_)
577            | ArithmeticValue::U32(_)
578            | ArithmeticValue::U64(_)
579            | ArithmeticValue::F32(_)
580            | ArithmeticValue::F64(_)
581    )
582}
583
584fn normalize_nan_f32(value: f32) -> f32 {
585    if value.is_nan() {
586        f32::from_bits(0x7fc0_0000)
587    } else {
588        value
589    }
590}
591
592fn normalize_nan_f64(value: f64) -> f64 {
593    if value.is_nan() {
594        f64::from_bits(0x7ff8_0000_0000_0000)
595    } else {
596        value
597    }
598}
599
600fn float_to_i64(value: f64) -> Result<i64> {
601    const I64_MIN_AS_F64: f64 = -9_223_372_036_854_775_808.0;
602    const I64_MAX_EXCLUSIVE_AS_F64: f64 = 9_223_372_036_854_775_808.0;
603    if !value.is_finite() || !(I64_MIN_AS_F64..I64_MAX_EXCLUSIVE_AS_F64).contains(&value.trunc()) {
604        return Err(XlogError::Compilation(
605            "Floating-point value cannot be represented by the runtime integer cast".to_string(),
606        ));
607    }
608    Ok(value as i64)
609}
610
611fn numeric_type_error() -> XlogError {
612    XlogError::Compilation("Arithmetic operation requires matching numeric types".to_string())
613}
614
615fn evaluation_state_error() -> XlogError {
616    XlogError::Compilation("Invalid arithmetic evaluation state".to_string())
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    fn evaluate(expression: &ArithExpr) -> Result<ArithmeticValue> {
624        evaluate_arithmetic_expression(expression, &HashMap::new())
625    }
626
627    #[test]
628    fn integer_edge_cases_match_runtime_contract() {
629        let zero = ArithExpr::Integer(0);
630        let one = ArithExpr::Integer(1);
631        assert_eq!(
632            evaluate(&ArithExpr::Div(
633                Box::new(one.clone()),
634                Box::new(zero.clone())
635            ))
636            .expect("division result"),
637            ArithmeticValue::I64(i64::MAX)
638        );
639        assert_eq!(
640            evaluate(&ArithExpr::Mod(Box::new(one), Box::new(zero))).expect("remainder result"),
641            ArithmeticValue::I64(0)
642        );
643        assert_eq!(
644            evaluate(&ArithExpr::Add(
645                Box::new(ArithExpr::Integer(i64::MAX)),
646                Box::new(ArithExpr::Integer(1)),
647            ))
648            .expect("wrapping sum"),
649            ArithmeticValue::I64(i64::MIN)
650        );
651
652        let bindings = HashMap::from([
653            (
654                "wide_unsigned".to_string(),
655                ArithmeticValue::U32(2_147_483_648),
656            ),
657            ("max_signed".to_string(), ArithmeticValue::I32(i32::MAX)),
658        ]);
659        assert_eq!(
660            evaluate_arithmetic_expression(
661                &ArithExpr::Add(
662                    Box::new(ArithExpr::Variable("wide_unsigned".to_string())),
663                    Box::new(ArithExpr::Variable("wide_unsigned".to_string())),
664                ),
665                &bindings,
666            )
667            .expect("u32 wrapping sum"),
668            ArithmeticValue::U32(0)
669        );
670        assert_eq!(
671            evaluate_arithmetic_expression(
672                &ArithExpr::Add(
673                    Box::new(ArithExpr::Variable("max_signed".to_string())),
674                    Box::new(ArithExpr::Cast(
675                        Box::new(ArithExpr::Integer(1)),
676                        ScalarType::I32,
677                    )),
678                ),
679                &bindings,
680            )
681            .expect("i32 wrapping sum"),
682            ArithmeticValue::I32(i32::MIN)
683        );
684    }
685
686    #[test]
687    fn casts_power_and_conditionals_preserve_types_and_order() {
688        let expression = ArithExpr::Conditional {
689            cond_left: Box::new(ArithExpr::Integer(1)),
690            cond_op: CompOp::Eq,
691            cond_right: Box::new(ArithExpr::Integer(1)),
692            then_expr: Box::new(ArithExpr::Cast(
693                Box::new(ArithExpr::Pow(
694                    Box::new(ArithExpr::Integer(2)),
695                    Box::new(ArithExpr::Integer(3)),
696                )),
697                ScalarType::F32,
698            )),
699            else_expr: Box::new(ArithExpr::Cast(
700                Box::new(ArithExpr::Integer(0)),
701                ScalarType::F32,
702            )),
703        };
704        assert_eq!(
705            evaluate(&expression).expect("conditional result"),
706            ArithmeticValue::F32(8.0)
707        );
708
709        let eager_error = ArithExpr::Conditional {
710            cond_left: Box::new(ArithExpr::Integer(1)),
711            cond_op: CompOp::Eq,
712            cond_right: Box::new(ArithExpr::Integer(1)),
713            then_expr: Box::new(ArithExpr::Variable("then_missing".to_string())),
714            else_expr: Box::new(ArithExpr::Variable("else_missing".to_string())),
715        };
716        let error = evaluate(&eager_error).expect_err("eager branch error");
717        assert!(error.to_string().contains("then_missing"), "{error}");
718
719        let bindings = HashMap::from([
720            ("enabled".to_string(), ArithmeticValue::Bool(true)),
721            ("label".to_string(), ArithmeticValue::Symbol(7)),
722        ]);
723        for (name, expected) in [("enabled", 1.0), ("label", 7.0)] {
724            let cast = ArithExpr::Cast(
725                Box::new(ArithExpr::Variable(name.to_string())),
726                ScalarType::F64,
727            );
728            assert_eq!(
729                evaluate_arithmetic_expression(&cast, &bindings).expect("runtime-compatible cast"),
730                ArithmeticValue::F64(expected)
731            );
732        }
733    }
734
735    #[test]
736    fn strings_and_symbols_remain_distinct_without_panicking() {
737        let mut bindings = HashMap::new();
738        bindings.insert(
739            "string_value".to_string(),
740            ArithmeticValue::String("value".to_string()),
741        );
742        bindings.insert("symbol_value".to_string(), ArithmeticValue::Symbol(7));
743        let comparison = ArithExpr::Conditional {
744            cond_left: Box::new(ArithExpr::Variable("string_value".to_string())),
745            cond_op: CompOp::Eq,
746            cond_right: Box::new(ArithExpr::Variable("symbol_value".to_string())),
747            then_expr: Box::new(ArithExpr::Integer(1)),
748            else_expr: Box::new(ArithExpr::Integer(0)),
749        };
750        let error = evaluate_arithmetic_expression(&comparison, &bindings)
751            .expect_err("string/symbol comparison must fail");
752        assert!(error.to_string().contains("differing types"), "{error}");
753
754        let cast = ArithExpr::Cast(
755            Box::new(ArithExpr::Variable("string_value".to_string())),
756            ScalarType::Symbol,
757        );
758        assert!(evaluate_arithmetic_expression(&cast, &bindings).is_err());
759    }
760
761    #[test]
762    fn float_operations_match_cuda_nan_and_selection_semantics() {
763        let nan = f64::from_bits(0xfff8_0000_0000_0042);
764        let mut bindings = HashMap::new();
765        bindings.insert("nan".to_string(), ArithmeticValue::F64(nan));
766        bindings.insert("one".to_string(), ArithmeticValue::F64(1.0));
767        bindings.insert("positive_zero".to_string(), ArithmeticValue::F64(0.0));
768        bindings.insert("negative_zero".to_string(), ArithmeticValue::F64(-0.0));
769
770        for expression in [
771            ArithExpr::Div(
772                Box::new(ArithExpr::Float(0.0)),
773                Box::new(ArithExpr::Float(0.0)),
774            ),
775            ArithExpr::Mod(
776                Box::new(ArithExpr::Float(0.0)),
777                Box::new(ArithExpr::Float(0.0)),
778            ),
779            ArithExpr::Pow(
780                Box::new(ArithExpr::Float(-1.0)),
781                Box::new(ArithExpr::Float(0.5)),
782            ),
783        ] {
784            let ArithmeticValue::F64(value) =
785                evaluate_arithmetic_expression(&expression, &bindings).expect("float result")
786            else {
787                panic!("expected f64 result");
788            };
789            assert_eq!(value.to_bits(), 0x7ff8_0000_0000_0000);
790        }
791
792        let ArithmeticValue::F64(minimum_nan) = evaluate_arithmetic_expression(
793            &ArithExpr::Min(
794                Box::new(ArithExpr::Variable("nan".to_string())),
795                Box::new(ArithExpr::Variable("one".to_string())),
796            ),
797            &bindings,
798        )
799        .expect("minimum") else {
800            panic!("expected f64 minimum");
801        };
802        assert_eq!(minimum_nan.to_bits(), nan.to_bits());
803        let ArithmeticValue::F64(minimum_zero) = evaluate_arithmetic_expression(
804            &ArithExpr::Min(
805                Box::new(ArithExpr::Variable("negative_zero".to_string())),
806                Box::new(ArithExpr::Variable("positive_zero".to_string())),
807            ),
808            &bindings,
809        )
810        .expect("minimum zero") else {
811            panic!("expected f64 minimum");
812        };
813        assert_eq!(minimum_zero.to_bits(), (-0.0_f64).to_bits());
814        let ArithmeticValue::F64(maximum_zero) = evaluate_arithmetic_expression(
815            &ArithExpr::Max(
816                Box::new(ArithExpr::Variable("positive_zero".to_string())),
817                Box::new(ArithExpr::Variable("negative_zero".to_string())),
818            ),
819            &bindings,
820        )
821        .expect("maximum zero") else {
822            panic!("expected f64 maximum");
823        };
824        assert_eq!(maximum_zero.to_bits(), 0.0_f64.to_bits());
825    }
826
827    #[test]
828    fn float_comparisons_use_runtime_promotion_and_total_ordering() {
829        let positive_nan = ArithmeticValue::F64(f64::from_bits(0x7ff8_0000_0000_0000));
830        assert!(compare_arithmetic_values(
831            &positive_nan,
832            CompOp::Gt,
833            &ArithmeticValue::F64(f64::INFINITY)
834        )
835        .expect("NaN ordering"));
836        assert!(
837            compare_arithmetic_values(&positive_nan, CompOp::Eq, &positive_nan)
838                .expect("NaN equality")
839        );
840        assert!(
841            !compare_arithmetic_values(&positive_nan, CompOp::Ne, &positive_nan)
842                .expect("NaN inequality")
843        );
844        assert!(compare_arithmetic_values(
845            &ArithmeticValue::F64(-0.0),
846            CompOp::Lt,
847            &ArithmeticValue::F64(0.0)
848        )
849        .expect("signed zero ordering"));
850        assert!(compare_arithmetic_values(
851            &ArithmeticValue::F32(2.5),
852            CompOp::Gt,
853            &ArithmeticValue::I64(2)
854        )
855        .expect("mixed numeric comparison"));
856
857        let conditional = ArithExpr::Conditional {
858            cond_left: Box::new(ArithExpr::Variable("float".to_string())),
859            cond_op: CompOp::Lt,
860            cond_right: Box::new(ArithExpr::Variable("integer".to_string())),
861            then_expr: Box::new(ArithExpr::Integer(1)),
862            else_expr: Box::new(ArithExpr::Integer(0)),
863        };
864        let bindings = HashMap::from([
865            ("float".to_string(), ArithmeticValue::F32(2.5)),
866            ("integer".to_string(), ArithmeticValue::I64(2)),
867        ]);
868        assert_eq!(
869            evaluate_arithmetic_expression(&conditional, &bindings).expect("conditional"),
870            ArithmeticValue::I64(0)
871        );
872
873        let mismatched_branches = ArithExpr::Conditional {
874            cond_left: Box::new(ArithExpr::Integer(1)),
875            cond_op: CompOp::Eq,
876            cond_right: Box::new(ArithExpr::Integer(1)),
877            then_expr: Box::new(ArithExpr::Integer(1)),
878            else_expr: Box::new(ArithExpr::Float(1.0)),
879        };
880        assert!(evaluate(&mismatched_branches).is_err());
881    }
882
883    #[test]
884    fn float_comparison_and_selection_follow_one_total_order() {
885        let f64_values = [
886            f64::from_bits(0xfff8_0000_0000_0002),
887            f64::NEG_INFINITY,
888            -0.0,
889            0.0,
890            f64::INFINITY,
891            f64::from_bits(0x7ff8_0000_0000_0001),
892            f64::from_bits(0x7ff8_0000_0000_0002),
893        ];
894        for &left in &f64_values {
895            for &right in &f64_values {
896                let ordering = left.total_cmp(&right);
897                for (op, expected) in [
898                    (CompOp::Eq, ordering.is_eq()),
899                    (CompOp::Ne, !ordering.is_eq()),
900                    (CompOp::Lt, ordering.is_lt()),
901                    (CompOp::Le, !ordering.is_gt()),
902                    (CompOp::Gt, ordering.is_gt()),
903                    (CompOp::Ge, !ordering.is_lt()),
904                ] {
905                    assert_eq!(
906                        compare_arithmetic_values(
907                            &ArithmeticValue::F64(left),
908                            op,
909                            &ArithmeticValue::F64(right),
910                        )
911                        .expect("f64 comparison"),
912                        expected,
913                        "f64 comparison mismatch: left={:#018x} op={op:?} right={:#018x}",
914                        left.to_bits(),
915                        right.to_bits(),
916                    );
917                }
918            }
919        }
920
921        let f32_values = [
922            f32::from_bits(0xffc0_0002),
923            f32::NEG_INFINITY,
924            -0.0,
925            0.0,
926            f32::INFINITY,
927            f32::from_bits(0x7fc0_0001),
928            f32::from_bits(0x7fc0_0002),
929        ];
930        for &left in &f32_values {
931            for &right in &f32_values {
932                let ordering = left.total_cmp(&right);
933                for (op, expected) in [
934                    (CompOp::Eq, ordering.is_eq()),
935                    (CompOp::Ne, !ordering.is_eq()),
936                    (CompOp::Lt, ordering.is_lt()),
937                    (CompOp::Le, !ordering.is_gt()),
938                    (CompOp::Gt, ordering.is_gt()),
939                    (CompOp::Ge, !ordering.is_lt()),
940                ] {
941                    assert_eq!(
942                        compare_arithmetic_values(
943                            &ArithmeticValue::F32(left),
944                            op,
945                            &ArithmeticValue::F32(right),
946                        )
947                        .expect("f32 comparison"),
948                        expected,
949                        "f32 comparison mismatch: left={:#010x} op={op:?} right={:#010x}",
950                        left.to_bits(),
951                        right.to_bits(),
952                    );
953                }
954            }
955        }
956
957        for (left, right) in [
958            (-0.0, 0.0),
959            (
960                f64::from_bits(0xfff8_0000_0000_0042),
961                f64::from_bits(0x7ff8_0000_0000_0024),
962            ),
963        ] {
964            for (first, second) in [(left, right), (right, left)] {
965                let bindings = HashMap::from([
966                    ("first".to_string(), ArithmeticValue::F64(first)),
967                    ("second".to_string(), ArithmeticValue::F64(second)),
968                ]);
969                for (expr, expected) in [
970                    (
971                        ArithExpr::Min(
972                            Box::new(ArithExpr::Variable("first".to_string())),
973                            Box::new(ArithExpr::Variable("second".to_string())),
974                        ),
975                        if left.total_cmp(&right).is_gt() {
976                            right
977                        } else {
978                            left
979                        },
980                    ),
981                    (
982                        ArithExpr::Max(
983                            Box::new(ArithExpr::Variable("first".to_string())),
984                            Box::new(ArithExpr::Variable("second".to_string())),
985                        ),
986                        if left.total_cmp(&right).is_lt() {
987                            right
988                        } else {
989                            left
990                        },
991                    ),
992                ] {
993                    let ArithmeticValue::F64(actual) =
994                        evaluate_arithmetic_expression(&expr, &bindings).expect("f64 selection")
995                    else {
996                        panic!("expected f64 selection result");
997                    };
998                    assert_eq!(actual.to_bits(), expected.to_bits());
999                }
1000            }
1001        }
1002    }
1003
1004    #[test]
1005    fn typed_terms_follow_lowering_widths_and_reject_undefined_float_casts() {
1006        assert_eq!(
1007            ArithmeticValue::from_typed_term(&Term::Integer(2_147_483_648), ScalarType::U32)
1008                .expect("u32 literal"),
1009            ArithmeticValue::U32(2_147_483_648)
1010        );
1011        assert_eq!(
1012            ArithmeticValue::from_typed_term(&Term::Float(1.5), ScalarType::F32)
1013                .expect("f32 literal"),
1014            ArithmeticValue::F32(1.5)
1015        );
1016        let invalid_cast = ArithExpr::Cast(Box::new(ArithExpr::Float(f64::NAN)), ScalarType::I64);
1017        assert!(evaluate(&invalid_cast).is_err());
1018    }
1019
1020    #[test]
1021    fn configured_depth_expression_uses_bounded_native_stack() {
1022        let mut expression = ArithExpr::Integer(0);
1023        for _ in 0..1_000 {
1024            expression = ArithExpr::Add(Box::new(expression), Box::new(ArithExpr::Integer(1)));
1025        }
1026        assert_eq!(
1027            evaluate(&expression).expect("deep expression result"),
1028            ArithmeticValue::I64(1_000)
1029        );
1030    }
1031}