Skip to main content

doge_runtime/ops/
compare.rs

1use std::cmp::Ordering;
2use std::rc::Rc;
3
4use bigdecimal::{BigDecimal, ToPrimitive};
5
6use crate::error::{DogeError, DogeResult};
7use crate::value::Value;
8
9/// Whether `v` is one of the three numeric types (`Int`/`Float`/`Decimal`), which
10/// compare across each other rather than by exact type.
11fn is_numeric(v: &Value) -> bool {
12    matches!(v, Value::Int(_) | Value::Float(_) | Value::Decimal(_))
13}
14
15/// A numeric value as `f64`, for any comparison that pairs a `Float` with an
16/// `Int`/`Decimal`. `None` only for a non-number or a `Float` NaN.
17fn numeric_f64(v: &Value) -> Option<f64> {
18    match v {
19        Value::Int(n) => n.to_f64(),
20        Value::Float(f) => Some(*f),
21        Value::Decimal(d) => d.to_f64(),
22        _ => None,
23    }
24}
25
26/// Ordering between two numeric values, comparing exactly where both are exact
27/// (Int/Decimal) and via `f64` once a `Float` is involved. `None` when a NaN is
28/// involved (no ordering). Both operands must be numeric.
29fn numeric_cmp(a: &Value, b: &Value) -> Option<Ordering> {
30    match (a, b) {
31        (Value::Int(x), Value::Int(y)) => Some(x.cmp(y)),
32        (Value::Decimal(x), Value::Decimal(y)) => Some(x.cmp(y)),
33        (Value::Int(x), Value::Decimal(y)) => Some(BigDecimal::from(x.clone()).cmp(y)),
34        (Value::Decimal(x), Value::Int(y)) => Some(x.cmp(&BigDecimal::from(y.clone()))),
35        _ => numeric_f64(a)?.partial_cmp(&numeric_f64(b)?),
36    }
37}
38
39/// Numeric equality when both operands are numbers (`Some`), else `None` so the
40/// caller falls through to structural/type comparison. Uses value ordering, so
41/// `dec("0.10") == dec("0.1")` and `1 == 1.0` both hold.
42fn numeric_equal(a: &Value, b: &Value) -> Option<bool> {
43    if is_numeric(a) && is_numeric(b) {
44        Some(numeric_cmp(a, b) == Some(Ordering::Equal))
45    } else {
46        None
47    }
48}
49
50/// Structural, Python-style equality: `1 == 1.0`, deep list/dict comparison,
51/// everything else across types is unequal.
52pub fn values_equal(a: &Value, b: &Value) -> bool {
53    if let Some(numeric) = numeric_equal(a, b) {
54        return numeric;
55    }
56    match (a, b) {
57        (Value::Str(x), Value::Str(y)) => x == y,
58        (Value::Bytes(x), Value::Bytes(y)) => x == y,
59        (Value::Bool(x), Value::Bool(y)) => x == y,
60        (Value::None, Value::None) => true,
61        (Value::List(x), Value::List(y)) => {
62            let (xb, yb) = (x.borrow(), y.borrow());
63            xb.len() == yb.len() && xb.iter().zip(yb.iter()).all(|(p, q)| values_equal(p, q))
64        }
65        (Value::Dict(x), Value::Dict(y)) => {
66            let (xb, yb) = (x.borrow(), y.borrow());
67            xb.len() == yb.len()
68                && xb
69                    .iter()
70                    .all(|(k, v)| yb.get(k).is_some_and(|w| values_equal(v, w)))
71        }
72        // Objects are equal only when they are the very same instance.
73        (Value::Object(x), Value::Object(y)) => Rc::ptr_eq(x, y),
74        // Functions are equal when they share a definition and the very same
75        // captured cells: `greet == greet` holds, but two closures built from the
76        // same definition over different environments do not.
77        (Value::Function(x), Value::Function(y)) => {
78            x.fn_id == y.fn_id
79                && x.captures.len() == y.captures.len()
80                && x.captures
81                    .iter()
82                    .zip(y.captures.iter())
83                    .all(|(p, q)| Rc::ptr_eq(p, q))
84        }
85        // Classes are equal when they name the same constructor: a class captures
86        // nothing, so `Shibe == Shibe` holds and `Shibe == Corgi` does not.
87        (Value::Class(x), Value::Class(y)) => x.fn_id == y.fn_id,
88        // Bound methods are equal when they name the same method on the very same
89        // receiver instance: `a.speak == a.speak` holds, but not `b.speak`.
90        (Value::BoundMethod(x), Value::BoundMethod(y)) => {
91            x.method == y.method && values_equal(&x.receiver, &y.receiver)
92        }
93        // Errors are equal when their type, message, and raise site all match.
94        (Value::Error(x), Value::Error(y)) => {
95            x.kind == y.kind && x.message == y.message && x.file == y.file && x.line == y.line
96        }
97        // Sockets are equal only when they are the very same handle.
98        (Value::Socket(x), Value::Socket(y)) => Rc::ptr_eq(x, y),
99        // Pups and bowls, like sockets, are equal only to the very same handle.
100        (Value::Pup(x), Value::Pup(y)) => Rc::ptr_eq(x, y),
101        (Value::Bowl(x), Value::Bowl(y)) => Rc::ptr_eq(x, y),
102        // Cross-type comparisons are simply unequal, never an error. Written by
103        // left-hand variant rather than a wildcard, so a new Value variant forces
104        // its own same-type case to be added above.
105        (Value::Int(_), _)
106        | (Value::Float(_), _)
107        | (Value::Decimal(_), _)
108        | (Value::Str(_), _)
109        | (Value::Bytes(_), _)
110        | (Value::Bool(_), _)
111        | (Value::None, _)
112        | (Value::List(_), _)
113        | (Value::Dict(_), _)
114        | (Value::Object(_), _)
115        | (Value::Function(_), _)
116        | (Value::Class(_), _)
117        | (Value::BoundMethod(_), _)
118        | (Value::Error(_), _)
119        | (Value::Socket(_), _)
120        | (Value::Pup(_), _)
121        | (Value::Bowl(_), _) => false,
122    }
123}
124
125/// `==`.
126pub fn eq(a: Value, b: Value) -> DogeResult {
127    Ok(Value::Bool(values_equal(&a, &b)))
128}
129
130/// `!=`.
131pub fn ne(a: Value, b: Value) -> DogeResult {
132    Ok(Value::Bool(!values_equal(&a, &b)))
133}
134
135/// Whether `target` is structurally equal to some element of `items`. Shared by
136/// the `in` operator and `List.contains` so their membership rule is identical.
137pub(crate) fn slice_contains(items: &[Value], target: &Value) -> bool {
138    items.iter().any(|element| values_equal(element, target))
139}
140
141/// `needle in container`. Python-style: a List tests element membership, a Dict
142/// tests key membership, a Str tests substring. Every other container type is a
143/// catchable type error. Matched by container variant (not a wildcard) so a new
144/// `Value` variant forces its own decision here.
145pub fn in_(needle: Value, container: Value) -> DogeResult {
146    let found = match &container {
147        Value::List(items) => slice_contains(&items.borrow(), &needle),
148        // A dict is keyed by Str; a non-Str needle can never be a key, so it is
149        // simply absent rather than an error (mirrors cross-type `==`).
150        Value::Dict(entries) => match &needle {
151            Value::Str(k) => entries.borrow().contains_key(k.as_ref()),
152            _ => false,
153        },
154        Value::Str(haystack) => match &needle {
155            Value::Str(sub) => haystack.contains(sub.as_ref()),
156            _ => {
157                return Err(DogeError::type_error(format!(
158                    "can only check if a Str is in a Str, not {}",
159                    needle.describe()
160                )));
161            }
162        },
163        // `int in bytes` tests byte membership (the byte must be 0–255); `bytes in
164        // bytes` tests for a contiguous sub-slice, mirroring `Str in Str`.
165        Value::Bytes(haystack) => match &needle {
166            Value::Int(n) => n.to_u8().is_some_and(|b| haystack.contains(&b)),
167            Value::Bytes(sub) => {
168                sub.is_empty() || haystack.windows(sub.len()).any(|w| w == &sub[..])
169            }
170            _ => {
171                return Err(DogeError::type_error(format!(
172                    "can only check if an Int or Bytes is in a Bytes, not {}",
173                    needle.describe()
174                )));
175            }
176        },
177        Value::Int(_)
178        | Value::Float(_)
179        | Value::Decimal(_)
180        | Value::Bool(_)
181        | Value::None
182        | Value::Object(_)
183        | Value::Function(_)
184        | Value::Class(_)
185        | Value::BoundMethod(_)
186        | Value::Error(_)
187        | Value::Socket(_)
188        | Value::Pup(_)
189        | Value::Bowl(_) => {
190            return Err(DogeError::type_error(format!(
191                "in wants a List, Dict, Str, or Bytes on the right, not {}",
192                container.describe()
193            )));
194        }
195    };
196    Ok(Value::Bool(found))
197}
198
199/// `needle not in container` — the negation of [`in_`], sharing its type rules so
200/// `x not in xs` and `not (x in xs)` always agree.
201pub fn not_in(needle: Value, container: Value) -> DogeResult {
202    match in_(needle, container)? {
203        Value::Bool(found) => Ok(Value::Bool(!found)),
204        _ => unreachable!("in_ always yields a Bool"),
205    }
206}
207
208/// Ordering for `< <= > >=`: numbers compare across Int/Float, Str compares
209/// lexicographically, anything else is a type error. The list `sort` method
210/// reuses this so its ordering matches the comparison operators exactly.
211pub(crate) fn order(a: &Value, b: &Value) -> DogeResult<Ordering> {
212    if is_numeric(a) && is_numeric(b) {
213        return numeric_cmp(a, b).ok_or_else(|| {
214            DogeError::type_error(format!(
215                "cannot compare {} and {}",
216                a.describe(),
217                b.describe()
218            ))
219        });
220    }
221    if let (Value::Str(x), Value::Str(y)) = (a, b) {
222        return Ok(x.as_ref().cmp(y.as_ref()));
223    }
224    if let (Value::Bytes(x), Value::Bytes(y)) = (a, b) {
225        return Ok(x.as_ref().cmp(y.as_ref()));
226    }
227    Err(DogeError::type_error(format!(
228        "cannot compare {} and {}",
229        a.describe(),
230        b.describe()
231    )))
232}
233
234/// `<`.
235pub fn lt(a: Value, b: Value) -> DogeResult {
236    Ok(Value::Bool(order(&a, &b)? == Ordering::Less))
237}
238
239/// `<=`.
240pub fn le(a: Value, b: Value) -> DogeResult {
241    Ok(Value::Bool(matches!(
242        order(&a, &b)?,
243        Ordering::Less | Ordering::Equal
244    )))
245}
246
247/// `>`.
248pub fn gt(a: Value, b: Value) -> DogeResult {
249    Ok(Value::Bool(order(&a, &b)? == Ordering::Greater))
250}
251
252/// `>=`.
253pub fn ge(a: Value, b: Value) -> DogeResult {
254    Ok(Value::Bool(matches!(
255        order(&a, &b)?,
256        Ordering::Greater | Ordering::Equal
257    )))
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn bytes_membership_subslice_and_ordering() {
266        let hay = Value::bytes([104, 105, 33]);
267        // `int in bytes` is byte membership; a value outside 0..=255 is simply absent.
268        assert!(matches!(
269            in_(Value::int(105), hay.clone()).unwrap(),
270            Value::Bool(true)
271        ));
272        assert!(matches!(
273            in_(Value::int(200), hay.clone()).unwrap(),
274            Value::Bool(false)
275        ));
276        assert!(matches!(
277            in_(Value::int(999), hay.clone()).unwrap(),
278            Value::Bool(false)
279        ));
280        // `bytes in bytes` is a contiguous sub-slice.
281        assert!(matches!(
282            in_(Value::bytes([105, 33]), hay.clone()).unwrap(),
283            Value::Bool(true)
284        ));
285        assert!(matches!(
286            in_(Value::bytes([104, 33]), hay.clone()).unwrap(),
287            Value::Bool(false)
288        ));
289        // Ordering is byte-wise.
290        assert_eq!(
291            order(&Value::bytes([1, 2]), &Value::bytes([1, 3])).unwrap(),
292            Ordering::Less
293        );
294    }
295
296    #[test]
297    fn bound_methods_equal_only_on_the_same_receiver_and_name() {
298        let a = Value::object(0, "Shibe");
299        let b = Value::object(0, "Shibe");
300        // Same instance, same method → equal.
301        assert!(values_equal(
302            &Value::bound_method(a.clone(), "speak"),
303            &Value::bound_method(a.clone(), "speak"),
304        ));
305        // Same instance, different method → not equal.
306        assert!(!values_equal(
307            &Value::bound_method(a.clone(), "speak"),
308            &Value::bound_method(a.clone(), "wag"),
309        ));
310        // Different instances of the same class → not equal.
311        assert!(!values_equal(
312            &Value::bound_method(a, "speak"),
313            &Value::bound_method(b, "speak"),
314        ));
315    }
316}