1use 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#[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 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
478pub 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}