use logicaffeine_base::numeric;
use logicaffeine_base::{BigInt, Rational};
use crate::ast::stmt::BinaryOpKind;
use crate::interpreter::RuntimeValue;
pub fn values_equal(left: &RuntimeValue, right: &RuntimeValue) -> bool {
values_equal_depth(left, right, 0)
}
const EQ_MAX_DEPTH: usize = 256;
fn values_equal_depth(left: &RuntimeValue, right: &RuntimeValue, depth: usize) -> bool {
if depth > EQ_MAX_DEPTH {
return false;
}
match (left, right) {
(RuntimeValue::Int(a), RuntimeValue::Int(b)) => a == b,
(RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => a == b,
(RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => a == b,
(RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) => a == b,
(RuntimeValue::Complex(a), RuntimeValue::Complex(b)) => a == b,
(RuntimeValue::Modular(a), RuntimeValue::Modular(b)) => a == b,
(RuntimeValue::Float(a), RuntimeValue::Float(b)) => a == b,
(RuntimeValue::Int(a), RuntimeValue::Float(b))
| (RuntimeValue::Float(b), RuntimeValue::Int(a)) => {
numeric::cmp_i64_f64_exact(*a, *b) == Some(std::cmp::Ordering::Equal)
}
(RuntimeValue::BigInt(a), RuntimeValue::Float(b))
| (RuntimeValue::Float(b), RuntimeValue::BigInt(a)) => {
numeric::cmp_bigint_f64_exact(a, *b) == Some(std::cmp::Ordering::Equal)
}
(RuntimeValue::Rational(a), RuntimeValue::Float(b))
| (RuntimeValue::Float(b), RuntimeValue::Rational(a)) => {
numeric::cmp_rational_f64_exact(a, *b) == Some(std::cmp::Ordering::Equal)
}
(RuntimeValue::Bool(a), RuntimeValue::Bool(b)) => a == b,
(RuntimeValue::Text(a), RuntimeValue::Text(b)) => **a == **b,
(RuntimeValue::Char(a), RuntimeValue::Char(b)) => a == b,
(RuntimeValue::Nothing, RuntimeValue::Nothing) => true,
(RuntimeValue::Duration(a), RuntimeValue::Duration(b)) => a == b,
(RuntimeValue::Date(a), RuntimeValue::Date(b)) => a == b,
(RuntimeValue::Moment(a), RuntimeValue::Moment(b)) => a == b,
(
RuntimeValue::Span { months: m1, days: d1 },
RuntimeValue::Span { months: m2, days: d2 },
) => m1 == m2 && d1 == d2,
(RuntimeValue::Time(a), RuntimeValue::Time(b)) => a == b,
(RuntimeValue::Word(a), RuntimeValue::Word(b)) => a == b,
(RuntimeValue::Money(a), RuntimeValue::Money(b)) => a == b,
(RuntimeValue::Quantity(a), RuntimeValue::Quantity(b)) => a.q == b.q,
(RuntimeValue::Uuid(a), RuntimeValue::Uuid(b)) => a == b,
(RuntimeValue::Inductive(a), RuntimeValue::Inductive(b)) => {
a.inductive_type == b.inductive_type
&& a.constructor == b.constructor
&& a.args.len() == b.args.len()
&& a.args.iter().zip(b.args.iter()).all(|(x, y)| values_equal_depth(x, y, depth + 1))
}
(RuntimeValue::List(a), RuntimeValue::List(b)) => {
if std::rc::Rc::ptr_eq(a, b) {
return true;
}
let (a, b) = (a.borrow(), b.borrow());
a.len() == b.len()
&& (0..a.len()).all(|i| match (a.get(i), b.get(i)) {
(Some(x), Some(y)) => values_equal_depth(&x, &y, depth + 1),
_ => false,
})
}
(RuntimeValue::Tuple(a), RuntimeValue::Tuple(b)) => {
a.len() == b.len()
&& a.iter().zip(b.iter()).all(|(x, y)| values_equal_depth(x, y, depth + 1))
}
(RuntimeValue::Set(a), RuntimeValue::Set(b)) => {
if std::rc::Rc::ptr_eq(a, b) {
return true;
}
let (a, b) = (a.borrow(), b.borrow());
a.len() == b.len()
&& a.iter().all(|x| b.iter().any(|y| values_equal_depth(x, y, depth + 1)))
}
(RuntimeValue::Map(a), RuntimeValue::Map(b)) => {
if std::rc::Rc::ptr_eq(a, b) {
return true;
}
let (a, b) = (a.borrow(), b.borrow());
a.len() == b.len()
&& a.iter().all(|(k, va)| {
b.get(k).is_some_and(|vb| values_equal_depth(va, vb, depth + 1))
})
}
(RuntimeValue::Struct(a), RuntimeValue::Struct(b)) => {
a.type_name == b.type_name
&& a.fields.len() == b.fields.len()
&& a.fields.iter().all(|(name, va)| {
b.fields.get(name).is_some_and(|vb| values_equal_depth(va, vb, depth + 1))
})
}
(RuntimeValue::Chan(a), RuntimeValue::Chan(b)) => a == b,
(RuntimeValue::TaskHandle(a), RuntimeValue::TaskHandle(b)) => a == b,
(RuntimeValue::Peer(a), RuntimeValue::Peer(b)) => **a == **b,
(RuntimeValue::Function(a), RuntimeValue::Function(b)) => a.body_index == b.body_index,
(RuntimeValue::Crdt(a), RuntimeValue::Crdt(b)) => {
crate::semantics::crdt::crdt_values_equal(&a.borrow(), &b.borrow())
}
(RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) => a == b,
_ => false,
}
}
pub fn compare(
op: BinaryOpKind,
left: &RuntimeValue,
right: &RuntimeValue,
) -> Result<RuntimeValue, String> {
use std::cmp::Ordering;
let rel = |ord: Option<Ordering>| -> bool {
match ord {
None => false,
Some(o) => match op {
BinaryOpKind::Lt => o == Ordering::Less,
BinaryOpKind::Gt => o == Ordering::Greater,
BinaryOpKind::LtEq => o != Ordering::Greater,
BinaryOpKind::GtEq => o != Ordering::Less,
_ => false,
},
}
};
let int_rel = |a: i64, b: i64| rel(Some(a.cmp(&b)));
match (left, right) {
(RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Bool(int_rel(*a, *b))),
(RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => {
Ok(RuntimeValue::Bool(rel(Some((**a).cmp(b)))))
}
(RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
Ok(RuntimeValue::Bool(rel(Some((**a).cmp(&BigInt::from_i64(*b))))))
}
(RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
Ok(RuntimeValue::Bool(rel(Some(BigInt::from_i64(*a).cmp(b)))))
}
(RuntimeValue::BigInt(a), RuntimeValue::Float(b)) => {
Ok(RuntimeValue::Bool(rel(numeric::cmp_bigint_f64_exact(a, *b))))
}
(RuntimeValue::Float(a), RuntimeValue::BigInt(b)) => {
Ok(RuntimeValue::Bool(rel(numeric::cmp_bigint_f64_exact(b, *a).map(std::cmp::Ordering::reverse))))
}
(RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => {
Ok(RuntimeValue::Bool(rel(Some((**a).cmp(b)))))
}
(RuntimeValue::Rational(a), RuntimeValue::Int(b)) => {
Ok(RuntimeValue::Bool(rel(Some((**a).cmp(&Rational::from_i64(*b))))))
}
(RuntimeValue::Int(a), RuntimeValue::Rational(b)) => {
Ok(RuntimeValue::Bool(rel(Some(Rational::from_i64(*a).cmp(b)))))
}
(RuntimeValue::Rational(a), RuntimeValue::BigInt(b)) => {
Ok(RuntimeValue::Bool(rel(Some((**a).cmp(&Rational::from_bigint((**b).clone()))))))
}
(RuntimeValue::BigInt(a), RuntimeValue::Rational(b)) => {
Ok(RuntimeValue::Bool(rel(Some(Rational::from_bigint((**a).clone()).cmp(b)))))
}
(RuntimeValue::Rational(a), RuntimeValue::Float(b)) => {
Ok(RuntimeValue::Bool(rel(numeric::cmp_rational_f64_exact(a, *b))))
}
(RuntimeValue::Float(a), RuntimeValue::Rational(b)) => {
Ok(RuntimeValue::Bool(rel(numeric::cmp_rational_f64_exact(b, *a).map(std::cmp::Ordering::reverse))))
}
(RuntimeValue::Float(a), RuntimeValue::Float(b)) => {
Ok(RuntimeValue::Bool(rel(a.partial_cmp(b))))
}
(RuntimeValue::Int(a), RuntimeValue::Float(b)) => {
Ok(RuntimeValue::Bool(rel(numeric::cmp_i64_f64_exact(*a, *b))))
}
(RuntimeValue::Float(a), RuntimeValue::Int(b)) => {
Ok(RuntimeValue::Bool(rel(numeric::cmp_i64_f64_exact(*b, *a).map(std::cmp::Ordering::reverse))))
}
(RuntimeValue::Duration(a), RuntimeValue::Duration(b)) => {
Ok(RuntimeValue::Bool(int_rel(*a, *b)))
}
(RuntimeValue::Date(a), RuntimeValue::Date(b)) => {
Ok(RuntimeValue::Bool(int_rel(*a as i64, *b as i64)))
}
(RuntimeValue::Moment(a), RuntimeValue::Moment(b)) => {
Ok(RuntimeValue::Bool(int_rel(*a, *b)))
}
(RuntimeValue::Time(a), RuntimeValue::Time(b)) => Ok(RuntimeValue::Bool(int_rel(*a, *b))),
(RuntimeValue::Moment(m), RuntimeValue::Time(t)) => {
let nanos_per_day = 86_400_000_000_000i64;
Ok(RuntimeValue::Bool(int_rel(m.rem_euclid(nanos_per_day), *t)))
}
(RuntimeValue::Time(t), RuntimeValue::Moment(m)) => {
let nanos_per_day = 86_400_000_000_000i64;
Ok(RuntimeValue::Bool(int_rel(*t, m.rem_euclid(nanos_per_day))))
}
(RuntimeValue::Quantity(a), RuntimeValue::Quantity(b)) => {
if a.q.dimension() != b.q.dimension() {
return Err(format!(
"Cannot compare quantities of different dimensions ({} vs {})",
a.q.dimension(),
b.q.dimension()
));
}
Ok(RuntimeValue::Bool(rel(Some(a.q.magnitude_si().cmp(b.q.magnitude_si())))))
}
(RuntimeValue::Money(a), RuntimeValue::Money(b)) => {
if a.currency != b.currency {
return Err(format!(
"cannot compare money of different currencies ({} vs {})",
a.currency.code, b.currency.code
));
}
Ok(RuntimeValue::Bool(rel(Some(
a.amount.to_rational().cmp(&b.amount.to_rational()),
))))
}
(RuntimeValue::Uuid(a), RuntimeValue::Uuid(b)) => Ok(RuntimeValue::Bool(rel(Some(a.cmp(b))))),
(l, r) if matches!(l, RuntimeValue::Decimal(_)) || matches!(r, RuntimeValue::Decimal(_)) => {
let rat_view = |v: &RuntimeValue| -> Option<Rational> {
match v {
RuntimeValue::Int(n) => Some(Rational::from_i64(*n)),
RuntimeValue::BigInt(b) => Some(Rational::from_bigint((**b).clone())),
RuntimeValue::Rational(r) => Some((**r).clone()),
RuntimeValue::Decimal(d) => Some(d.to_rational()),
_ => None,
}
};
if let (Some(a), Some(b)) = (rat_view(l), rat_view(r)) {
Ok(RuntimeValue::Bool(rel(Some(a.cmp(&b)))))
} else {
let f64_view = |v: &RuntimeValue| -> Option<f64> {
match v {
RuntimeValue::Float(f) => Some(*f),
other => rat_view(other).map(|x| x.to_f64()),
}
};
match (f64_view(l), f64_view(r)) {
(Some(a), Some(b)) => Ok(RuntimeValue::Bool(rel(a.partial_cmp(&b)))),
_ => Err(format!("Cannot compare {} and {}", l.type_name(), r.type_name())),
}
}
}
_ => Err(format!(
"Cannot compare {} and {}",
left.type_name(),
right.type_name()
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn float_equality_is_ieee_and_nan_unequal() {
let sum = RuntimeValue::Float(0.1 + 0.2);
assert!(!values_equal(&sum, &RuntimeValue::Float(0.3)));
assert!(values_equal(&sum, &RuntimeValue::Float(0.30000000000000004)));
assert!(!values_equal(&RuntimeValue::Float(f64::NAN), &RuntimeValue::Float(f64::NAN)));
assert!(values_equal(&RuntimeValue::Int(1), &RuntimeValue::Float(1.0)));
assert!(!values_equal(
&RuntimeValue::Int(9_007_199_254_740_993),
&RuntimeValue::Float(9_007_199_254_740_992.0)
));
}
#[test]
fn collections_compare_structurally() {
use std::cell::RefCell;
use std::rc::Rc;
let list = |vals: Vec<i64>| {
RuntimeValue::List(Rc::new(RefCell::new(
crate::interpreter::ListRepr::from_values(
vals.into_iter().map(RuntimeValue::Int).collect(),
),
)))
};
assert!(values_equal(&list(vec![1, 2, 3]), &list(vec![1, 2, 3])));
assert!(!values_equal(&list(vec![1, 2, 3]), &list(vec![1, 2, 4])));
assert!(!values_equal(&list(vec![1, 2]), &list(vec![1, 2, 3])));
}
#[test]
fn nan_relational_comparisons_are_false_not_errors() {
let nan = RuntimeValue::Float(f64::NAN);
for op in [BinaryOpKind::Lt, BinaryOpKind::Gt, BinaryOpKind::LtEq, BinaryOpKind::GtEq] {
let r = compare(op, &nan, &RuntimeValue::Float(1.0)).unwrap();
assert!(matches!(r, RuntimeValue::Bool(false)));
}
}
#[test]
fn moment_compares_to_time_by_time_of_day() {
let nanos_per_day = 86_400_000_000_000i64;
let m = RuntimeValue::Moment(3 * nanos_per_day + 10 * 3_600_000_000_000);
let t = RuntimeValue::Time(11 * 3_600_000_000_000);
assert!(matches!(compare(BinaryOpKind::Lt, &m, &t).unwrap(), RuntimeValue::Bool(true)));
assert!(matches!(compare(BinaryOpKind::Gt, &t, &m).unwrap(), RuntimeValue::Bool(true)));
}
#[test]
fn comparison_type_error_message() {
let e = compare(BinaryOpKind::Lt, &RuntimeValue::Bool(true), &RuntimeValue::Int(1))
.unwrap_err();
assert_eq!(e, "Cannot compare Bool and Int");
}
}