1use 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#[derive(Debug, Clone, PartialEq)]
13pub enum ArithmeticValue {
14 I32(i32),
16 I64(i64),
18 U32(u32),
20 U64(u64),
22 F32(f32),
24 F64(f64),
26 Bool(bool),
28 Symbol(u32),
30 String(String),
32}
33
34impl ArithmeticValue {
35 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 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 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 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
211pub 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
478pub 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}