Skip to main content

logicaffeine_compile/semantics/
compare.rs

1//! Equality and relational comparison.
2
3use logicaffeine_base::numeric;
4use logicaffeine_base::{BigInt, Rational};
5
6use crate::ast::stmt::BinaryOpKind;
7use crate::interpreter::RuntimeValue;
8
9/// Value equality for the `equals`/`==` operator, set/list membership, and
10/// map keys (`RuntimeValue`'s `PartialEq` delegates here — ONE equality).
11///
12/// - Floats compare by IEEE `==` (`NaN != NaN`, `-0.0 == 0.0`) — identical to
13///   what compiled Rust emits, on every engine.
14/// - Cross-type numeric equality is EXACT (mathematical value, never a lossy
15///   cast): `1 == 1.0`, but `9007199254740993 != 9007199254740993.0` because
16///   that float literal IS `2^53`. Coheres with `compare` and with the
17///   unified numeric hash (`base::numeric`).
18/// - Collections, tuples, and structs compare STRUCTURALLY (same shape, all
19///   parts equal), with an `Rc` identity fast path and a depth cap against
20///   cyclic values.
21/// - Decimal/Complex/Modular keep their documented within-type equality
22///   (they have no cross-type ordering, so there is no coherence to break).
23pub fn values_equal(left: &RuntimeValue, right: &RuntimeValue) -> bool {
24    values_equal_depth(left, right, 0)
25}
26
27/// Cyclic values (a list pushed into itself) bottom out here instead of
28/// overflowing the stack; 256 levels is far beyond any real data shape.
29const EQ_MAX_DEPTH: usize = 256;
30
31fn values_equal_depth(left: &RuntimeValue, right: &RuntimeValue, depth: usize) -> bool {
32    if depth > EQ_MAX_DEPTH {
33        return false;
34    }
35    match (left, right) {
36        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => a == b,
37        // BigInt holds only out-of-i64 values, so a BigInt never equals an Int; two
38        // BigInts compare by exact magnitude (the `_ => false` arm would be wrong).
39        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => a == b,
40        // A Rational is never whole, so it never equals an Int/BigInt; Rational==Rational
41        // is exact (reduced form is canonical).
42        (RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => a == b,
43        // Decimal equality is by VALUE (`1.0 == 1.00`); like Int vs Float, a Decimal is
44        // never `==` a value of a different numeric type.
45        (RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) => a == b,
46        // Complex equality is exact and structural; complex numbers have no ordering, so
47        // `compare` (`< > …`) deliberately falls through to a type error.
48        (RuntimeValue::Complex(a), RuntimeValue::Complex(b)) => a == b,
49        // Modular equality is per-ring (same residue AND modulus); ℤ/nℤ has no total order,
50        // so `compare` falls through to a type error.
51        (RuntimeValue::Modular(a), RuntimeValue::Modular(b)) => a == b,
52        // IEEE float equality — what the compiled backend emits.
53        (RuntimeValue::Float(a), RuntimeValue::Float(b)) => a == b,
54        // Exact cross-type numeric equality (mathematical values).
55        (RuntimeValue::Int(a), RuntimeValue::Float(b))
56        | (RuntimeValue::Float(b), RuntimeValue::Int(a)) => {
57            numeric::cmp_i64_f64_exact(*a, *b) == Some(std::cmp::Ordering::Equal)
58        }
59        (RuntimeValue::BigInt(a), RuntimeValue::Float(b))
60        | (RuntimeValue::Float(b), RuntimeValue::BigInt(a)) => {
61            numeric::cmp_bigint_f64_exact(a, *b) == Some(std::cmp::Ordering::Equal)
62        }
63        (RuntimeValue::Rational(a), RuntimeValue::Float(b))
64        | (RuntimeValue::Float(b), RuntimeValue::Rational(a)) => {
65            numeric::cmp_rational_f64_exact(a, *b) == Some(std::cmp::Ordering::Equal)
66        }
67        (RuntimeValue::Bool(a), RuntimeValue::Bool(b)) => a == b,
68        (RuntimeValue::Text(a), RuntimeValue::Text(b)) => **a == **b,
69        (RuntimeValue::Char(a), RuntimeValue::Char(b)) => a == b,
70        (RuntimeValue::Nothing, RuntimeValue::Nothing) => true,
71        (RuntimeValue::Duration(a), RuntimeValue::Duration(b)) => a == b,
72        (RuntimeValue::Date(a), RuntimeValue::Date(b)) => a == b,
73        (RuntimeValue::Moment(a), RuntimeValue::Moment(b)) => a == b,
74        (
75            RuntimeValue::Span { months: m1, days: d1 },
76            RuntimeValue::Span { months: m2, days: d2 },
77        ) => m1 == m2 && d1 == d2,
78        (RuntimeValue::Time(a), RuntimeValue::Time(b)) => a == b,
79        // Words are equal only at the same width and value (`WordVal`'s derived `Eq`).
80        (RuntimeValue::Word(a), RuntimeValue::Word(b)) => a == b,
81        // Money is value-equal by currency + amount; a Quantity by physical magnitude (display unit
82        // ignored, so `2 inches == 5.08 cm`); a Uuid by its 128 bits.
83        (RuntimeValue::Money(a), RuntimeValue::Money(b)) => a == b,
84        (RuntimeValue::Quantity(a), RuntimeValue::Quantity(b)) => a.q == b.q,
85        (RuntimeValue::Uuid(a), RuntimeValue::Uuid(b)) => a == b,
86        (RuntimeValue::Inductive(a), RuntimeValue::Inductive(b)) => {
87            a.inductive_type == b.inductive_type
88                && a.constructor == b.constructor
89                && a.args.len() == b.args.len()
90                && a.args.iter().zip(b.args.iter()).all(|(x, y)| values_equal_depth(x, y, depth + 1))
91        }
92        // ── Structural equality ─────────────────────────────────────────
93        (RuntimeValue::List(a), RuntimeValue::List(b)) => {
94            if std::rc::Rc::ptr_eq(a, b) {
95                return true;
96            }
97            let (a, b) = (a.borrow(), b.borrow());
98            a.len() == b.len()
99                && (0..a.len()).all(|i| match (a.get(i), b.get(i)) {
100                    (Some(x), Some(y)) => values_equal_depth(&x, &y, depth + 1),
101                    _ => false,
102                })
103        }
104        (RuntimeValue::Tuple(a), RuntimeValue::Tuple(b)) => {
105            a.len() == b.len()
106                && a.iter().zip(b.iter()).all(|(x, y)| values_equal_depth(x, y, depth + 1))
107        }
108        // Sets compare by CONTENT, order-insensitive (both sides are deduped,
109        // so equal length + one-sided containment is a bijection).
110        (RuntimeValue::Set(a), RuntimeValue::Set(b)) => {
111            if std::rc::Rc::ptr_eq(a, b) {
112                return true;
113            }
114            let (a, b) = (a.borrow(), b.borrow());
115            a.len() == b.len()
116                && a.iter().all(|x| b.iter().any(|y| values_equal_depth(x, y, depth + 1)))
117        }
118        // Maps compare by CONTENT, insertion order ignored (like Python dicts).
119        (RuntimeValue::Map(a), RuntimeValue::Map(b)) => {
120            if std::rc::Rc::ptr_eq(a, b) {
121                return true;
122            }
123            let (a, b) = (a.borrow(), b.borrow());
124            a.len() == b.len()
125                && a.iter().all(|(k, va)| {
126                    b.get(k).is_some_and(|vb| values_equal_depth(va, vb, depth + 1))
127                })
128        }
129        (RuntimeValue::Struct(a), RuntimeValue::Struct(b)) => {
130            a.type_name == b.type_name
131                && a.fields.len() == b.fields.len()
132                && a.fields.iter().all(|(name, va)| {
133                    b.fields.get(name).is_some_and(|vb| values_equal_depth(va, vb, depth + 1))
134                })
135        }
136        // ── Identity-style values (moved from the old `PartialEq` impl so
137        //    this function is the TOTAL equality it delegates to) ─────────
138        (RuntimeValue::Chan(a), RuntimeValue::Chan(b)) => a == b,
139        (RuntimeValue::TaskHandle(a), RuntimeValue::TaskHandle(b)) => a == b,
140        (RuntimeValue::Peer(a), RuntimeValue::Peer(b)) => **a == **b,
141        (RuntimeValue::Function(a), RuntimeValue::Function(b)) => a.body_index == b.body_index,
142        // Two CRDTs are equal when they observe the same elements (a sequence also
143        // compares order) — the convergence-relevant view, ignoring internal tags.
144        (RuntimeValue::Crdt(a), RuntimeValue::Crdt(b)) => {
145            crate::semantics::crdt::crdt_values_equal(&a.borrow(), &b.borrow())
146        }
147        // Lane vectors compare by value (all lanes equal), consistent with Hash.
148        (RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) => a == b,
149        _ => false,
150    }
151}
152
153/// Relational comparison (`< > <= >=`).
154///
155/// Floats use IEEE 754 semantics — NaN is unordered (every relational
156/// comparison with a NaN is `false`) and `-0.0 == 0.0` — matching Rust's `f64`
157/// ordering, which is what the compile-to-Rust path emits. Integer and
158/// temporal types use the natural total order; a Moment compares against a
159/// Time by its time-of-day.
160pub fn compare(
161    op: BinaryOpKind,
162    left: &RuntimeValue,
163    right: &RuntimeValue,
164) -> Result<RuntimeValue, String> {
165    use std::cmp::Ordering;
166
167    // Map an `Ordering` (or `None` for an unordered/NaN pair) to the operator.
168    let rel = |ord: Option<Ordering>| -> bool {
169        match ord {
170            None => false,
171            Some(o) => match op {
172                BinaryOpKind::Lt => o == Ordering::Less,
173                BinaryOpKind::Gt => o == Ordering::Greater,
174                BinaryOpKind::LtEq => o != Ordering::Greater,
175                BinaryOpKind::GtEq => o != Ordering::Less,
176                _ => false,
177            },
178        }
179    };
180    let int_rel = |a: i64, b: i64| rel(Some(a.cmp(&b)));
181
182    match (left, right) {
183        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Bool(int_rel(*a, *b))),
184        // Exact integer ordering across the narrow/wide boundary: compare as BigInts.
185        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => {
186            Ok(RuntimeValue::Bool(rel(Some((**a).cmp(b)))))
187        }
188        (RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
189            Ok(RuntimeValue::Bool(rel(Some((**a).cmp(&BigInt::from_i64(*b))))))
190        }
191        (RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
192            Ok(RuntimeValue::Bool(rel(Some(BigInt::from_i64(*a).cmp(b)))))
193        }
194        // Cross-type numeric ordering is EXACT — mathematical values, never a
195        // lossy as-f64 view (which rounds above 2^53). NaN stays unordered.
196        (RuntimeValue::BigInt(a), RuntimeValue::Float(b)) => {
197            Ok(RuntimeValue::Bool(rel(numeric::cmp_bigint_f64_exact(a, *b))))
198        }
199        (RuntimeValue::Float(a), RuntimeValue::BigInt(b)) => {
200            Ok(RuntimeValue::Bool(rel(numeric::cmp_bigint_f64_exact(b, *a).map(std::cmp::Ordering::reverse))))
201        }
202        // Exact rational ordering (cross-multiply, no rounding) including the
203        // narrow/wide boundary; vs Float uses IEEE partial order on the f64 view.
204        (RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => {
205            Ok(RuntimeValue::Bool(rel(Some((**a).cmp(b)))))
206        }
207        (RuntimeValue::Rational(a), RuntimeValue::Int(b)) => {
208            Ok(RuntimeValue::Bool(rel(Some((**a).cmp(&Rational::from_i64(*b))))))
209        }
210        (RuntimeValue::Int(a), RuntimeValue::Rational(b)) => {
211            Ok(RuntimeValue::Bool(rel(Some(Rational::from_i64(*a).cmp(b)))))
212        }
213        (RuntimeValue::Rational(a), RuntimeValue::BigInt(b)) => {
214            Ok(RuntimeValue::Bool(rel(Some((**a).cmp(&Rational::from_bigint((**b).clone()))))))
215        }
216        (RuntimeValue::BigInt(a), RuntimeValue::Rational(b)) => {
217            Ok(RuntimeValue::Bool(rel(Some(Rational::from_bigint((**a).clone()).cmp(b)))))
218        }
219        (RuntimeValue::Rational(a), RuntimeValue::Float(b)) => {
220            Ok(RuntimeValue::Bool(rel(numeric::cmp_rational_f64_exact(a, *b))))
221        }
222        (RuntimeValue::Float(a), RuntimeValue::Rational(b)) => {
223            Ok(RuntimeValue::Bool(rel(numeric::cmp_rational_f64_exact(b, *a).map(std::cmp::Ordering::reverse))))
224        }
225        (RuntimeValue::Float(a), RuntimeValue::Float(b)) => {
226            Ok(RuntimeValue::Bool(rel(a.partial_cmp(b))))
227        }
228        (RuntimeValue::Int(a), RuntimeValue::Float(b)) => {
229            Ok(RuntimeValue::Bool(rel(numeric::cmp_i64_f64_exact(*a, *b))))
230        }
231        (RuntimeValue::Float(a), RuntimeValue::Int(b)) => {
232            Ok(RuntimeValue::Bool(rel(numeric::cmp_i64_f64_exact(*b, *a).map(std::cmp::Ordering::reverse))))
233        }
234        (RuntimeValue::Duration(a), RuntimeValue::Duration(b)) => {
235            Ok(RuntimeValue::Bool(int_rel(*a, *b)))
236        }
237        (RuntimeValue::Date(a), RuntimeValue::Date(b)) => {
238            Ok(RuntimeValue::Bool(int_rel(*a as i64, *b as i64)))
239        }
240        (RuntimeValue::Moment(a), RuntimeValue::Moment(b)) => {
241            Ok(RuntimeValue::Bool(int_rel(*a, *b)))
242        }
243        (RuntimeValue::Time(a), RuntimeValue::Time(b)) => Ok(RuntimeValue::Bool(int_rel(*a, *b))),
244        // Moment vs Time: extract time-of-day from Moment. Use Euclidean
245        // remainder so a pre-epoch (negative) Moment yields a 0..86399 ns
246        // time-of-day, not a negative one.
247        (RuntimeValue::Moment(m), RuntimeValue::Time(t)) => {
248            let nanos_per_day = 86_400_000_000_000i64;
249            Ok(RuntimeValue::Bool(int_rel(m.rem_euclid(nanos_per_day), *t)))
250        }
251        (RuntimeValue::Time(t), RuntimeValue::Moment(m)) => {
252            let nanos_per_day = 86_400_000_000_000i64;
253            Ok(RuntimeValue::Bool(int_rel(*t, m.rem_euclid(nanos_per_day))))
254        }
255        // Two physical quantities order by their EXACT SI magnitude when their dimensions match;
256        // ordering across dimensions (Length vs Mass) is a typed error, mirroring the AOT `PartialOrd`
257        // (cross-dimension → `None` → not orderable). `==`/`!=` go through value equality elsewhere.
258        (RuntimeValue::Quantity(a), RuntimeValue::Quantity(b)) => {
259            if a.q.dimension() != b.q.dimension() {
260                return Err(format!(
261                    "Cannot compare quantities of different dimensions ({} vs {})",
262                    a.q.dimension(),
263                    b.q.dimension()
264                ));
265            }
266            Ok(RuntimeValue::Bool(rel(Some(a.q.magnitude_si().cmp(b.q.magnitude_si())))))
267        }
268        // Money orders by amount within the SAME currency; ordering across currencies is meaningless
269        // without a rate context, so it is a typed error (the dimension-mismatch precedent).
270        (RuntimeValue::Money(a), RuntimeValue::Money(b)) => {
271            if a.currency != b.currency {
272                return Err(format!(
273                    "cannot compare money of different currencies ({} vs {})",
274                    a.currency.code, b.currency.code
275                ));
276            }
277            Ok(RuntimeValue::Bool(rel(Some(
278                a.amount.to_rational().cmp(&b.amount.to_rational()),
279            ))))
280        }
281        // UUIDs order by their 128 bits — so v6/v7 (time-ordered) ids sort chronologically.
282        (RuntimeValue::Uuid(a), RuntimeValue::Uuid(b)) => Ok(RuntimeValue::Bool(rel(Some(a.cmp(b))))),
283        // A Decimal orders by EXACT value against any exact number (Int/BigInt/Rational/
284        // Decimal); against a Float it compares on the f64 view (IEEE partial order).
285        (l, r) if matches!(l, RuntimeValue::Decimal(_)) || matches!(r, RuntimeValue::Decimal(_)) => {
286            let rat_view = |v: &RuntimeValue| -> Option<Rational> {
287                match v {
288                    RuntimeValue::Int(n) => Some(Rational::from_i64(*n)),
289                    RuntimeValue::BigInt(b) => Some(Rational::from_bigint((**b).clone())),
290                    RuntimeValue::Rational(r) => Some((**r).clone()),
291                    RuntimeValue::Decimal(d) => Some(d.to_rational()),
292                    _ => None,
293                }
294            };
295            if let (Some(a), Some(b)) = (rat_view(l), rat_view(r)) {
296                Ok(RuntimeValue::Bool(rel(Some(a.cmp(&b)))))
297            } else {
298                let f64_view = |v: &RuntimeValue| -> Option<f64> {
299                    match v {
300                        RuntimeValue::Float(f) => Some(*f),
301                        other => rat_view(other).map(|x| x.to_f64()),
302                    }
303                };
304                match (f64_view(l), f64_view(r)) {
305                    (Some(a), Some(b)) => Ok(RuntimeValue::Bool(rel(a.partial_cmp(&b)))),
306                    _ => Err(format!("Cannot compare {} and {}", l.type_name(), r.type_name())),
307                }
308            }
309        }
310        _ => Err(format!(
311            "Cannot compare {} and {}",
312            left.type_name(),
313            right.type_name()
314        )),
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn float_equality_is_ieee_and_nan_unequal() {
324        // IEEE bit equality — the float artifact is REAL and visible
325        // (`is approximately` is the tolerant spelling), identical to what
326        // the compiled backend emits.
327        let sum = RuntimeValue::Float(0.1 + 0.2);
328        assert!(!values_equal(&sum, &RuntimeValue::Float(0.3)));
329        assert!(values_equal(&sum, &RuntimeValue::Float(0.30000000000000004)));
330        assert!(!values_equal(&RuntimeValue::Float(f64::NAN), &RuntimeValue::Float(f64::NAN)));
331        // Cross-type numeric equality is EXACT: `1 == 1.0` …
332        assert!(values_equal(&RuntimeValue::Int(1), &RuntimeValue::Float(1.0)));
333        // … but never a lossy cast: 2^53 + 1 is NOT the float 2^53.
334        assert!(!values_equal(
335            &RuntimeValue::Int(9_007_199_254_740_993),
336            &RuntimeValue::Float(9_007_199_254_740_992.0)
337        ));
338    }
339
340    #[test]
341    fn collections_compare_structurally() {
342        use std::cell::RefCell;
343        use std::rc::Rc;
344        let list = |vals: Vec<i64>| {
345            RuntimeValue::List(Rc::new(RefCell::new(
346                crate::interpreter::ListRepr::from_values(
347                    vals.into_iter().map(RuntimeValue::Int).collect(),
348                ),
349            )))
350        };
351        // Same contents ⇒ equal (the audited `[1,2,3] == [1,2,3]` row).
352        assert!(values_equal(&list(vec![1, 2, 3]), &list(vec![1, 2, 3])));
353        // Different contents or length ⇒ unequal.
354        assert!(!values_equal(&list(vec![1, 2, 3]), &list(vec![1, 2, 4])));
355        assert!(!values_equal(&list(vec![1, 2]), &list(vec![1, 2, 3])));
356    }
357
358    #[test]
359    fn nan_relational_comparisons_are_false_not_errors() {
360        let nan = RuntimeValue::Float(f64::NAN);
361        for op in [BinaryOpKind::Lt, BinaryOpKind::Gt, BinaryOpKind::LtEq, BinaryOpKind::GtEq] {
362            let r = compare(op, &nan, &RuntimeValue::Float(1.0)).unwrap();
363            assert!(matches!(r, RuntimeValue::Bool(false)));
364        }
365    }
366
367    #[test]
368    fn moment_compares_to_time_by_time_of_day() {
369        let nanos_per_day = 86_400_000_000_000i64;
370        // A moment at 10:00 into some day vs a time of 11:00.
371        let m = RuntimeValue::Moment(3 * nanos_per_day + 10 * 3_600_000_000_000);
372        let t = RuntimeValue::Time(11 * 3_600_000_000_000);
373        assert!(matches!(compare(BinaryOpKind::Lt, &m, &t).unwrap(), RuntimeValue::Bool(true)));
374        assert!(matches!(compare(BinaryOpKind::Gt, &t, &m).unwrap(), RuntimeValue::Bool(true)));
375    }
376
377    #[test]
378    fn comparison_type_error_message() {
379        let e = compare(BinaryOpKind::Lt, &RuntimeValue::Bool(true), &RuntimeValue::Int(1))
380            .unwrap_err();
381        assert_eq!(e, "Cannot compare Bool and Int");
382    }
383}