Skip to main content

doge_runtime/ops/
compare.rs

1use std::cmp::Ordering;
2use std::rc::Rc;
3
4use super::as_f64;
5use crate::error::{DogeError, DogeResult};
6use crate::value::Value;
7
8/// Structural, Python-style equality: `1 == 1.0`, deep list/dict comparison,
9/// everything else across types is unequal.
10pub fn values_equal(a: &Value, b: &Value) -> bool {
11    match (a, b) {
12        (Value::Int(x), Value::Int(y)) => x == y,
13        (Value::Float(x), Value::Float(y)) => x == y,
14        (Value::Int(x), Value::Float(y)) => (*x as f64) == *y,
15        (Value::Float(x), Value::Int(y)) => *x == (*y as f64),
16        (Value::Str(x), Value::Str(y)) => x == y,
17        (Value::Bool(x), Value::Bool(y)) => x == y,
18        (Value::None, Value::None) => true,
19        (Value::List(x), Value::List(y)) => {
20            let (xb, yb) = (x.borrow(), y.borrow());
21            xb.len() == yb.len() && xb.iter().zip(yb.iter()).all(|(p, q)| values_equal(p, q))
22        }
23        (Value::Dict(x), Value::Dict(y)) => {
24            let (xb, yb) = (x.borrow(), y.borrow());
25            xb.len() == yb.len()
26                && xb
27                    .iter()
28                    .all(|(k, v)| yb.get(k).is_some_and(|w| values_equal(v, w)))
29        }
30        // Objects are equal only when they are the very same instance.
31        (Value::Object(x), Value::Object(y)) => Rc::ptr_eq(x, y),
32        // Functions are equal when they share a definition and the very same
33        // captured cells: `greet == greet` holds, but two closures built from the
34        // same definition over different environments do not.
35        (Value::Function(x), Value::Function(y)) => {
36            x.fn_id == y.fn_id
37                && x.captures.len() == y.captures.len()
38                && x.captures
39                    .iter()
40                    .zip(y.captures.iter())
41                    .all(|(p, q)| Rc::ptr_eq(p, q))
42        }
43        // Classes are equal when they name the same constructor: a class captures
44        // nothing, so `Shibe == Shibe` holds and `Shibe == Corgi` does not.
45        (Value::Class(x), Value::Class(y)) => x.fn_id == y.fn_id,
46        // Bound methods are equal when they name the same method on the very same
47        // receiver instance: `a.speak == a.speak` holds, but not `b.speak`.
48        (Value::BoundMethod(x), Value::BoundMethod(y)) => {
49            x.method == y.method && values_equal(&x.receiver, &y.receiver)
50        }
51        // Errors are equal when their type, message, and raise site all match.
52        (Value::Error(x), Value::Error(y)) => {
53            x.kind == y.kind && x.message == y.message && x.file == y.file && x.line == y.line
54        }
55        // Cross-type comparisons are simply unequal, never an error. Written by
56        // left-hand variant rather than a wildcard, so a new Value variant forces
57        // its own same-type case to be added above.
58        (Value::Int(_), _)
59        | (Value::Float(_), _)
60        | (Value::Str(_), _)
61        | (Value::Bool(_), _)
62        | (Value::None, _)
63        | (Value::List(_), _)
64        | (Value::Dict(_), _)
65        | (Value::Object(_), _)
66        | (Value::Function(_), _)
67        | (Value::Class(_), _)
68        | (Value::BoundMethod(_), _)
69        | (Value::Error(_), _) => false,
70    }
71}
72
73/// `==`.
74pub fn eq(a: Value, b: Value) -> DogeResult {
75    Ok(Value::Bool(values_equal(&a, &b)))
76}
77
78/// `!=`.
79pub fn ne(a: Value, b: Value) -> DogeResult {
80    Ok(Value::Bool(!values_equal(&a, &b)))
81}
82
83/// Whether `target` is structurally equal to some element of `items`. Shared by
84/// the `in` operator and `List.contains` so their membership rule is identical.
85pub(crate) fn slice_contains(items: &[Value], target: &Value) -> bool {
86    items.iter().any(|element| values_equal(element, target))
87}
88
89/// `needle in container`. Python-style: a List tests element membership, a Dict
90/// tests key membership, a Str tests substring. Every other container type is a
91/// catchable type error. Matched by container variant (not a wildcard) so a new
92/// `Value` variant forces its own decision here.
93pub fn in_(needle: Value, container: Value) -> DogeResult {
94    let found = match &container {
95        Value::List(items) => slice_contains(&items.borrow(), &needle),
96        // A dict is keyed by Str; a non-Str needle can never be a key, so it is
97        // simply absent rather than an error (mirrors cross-type `==`).
98        Value::Dict(entries) => match &needle {
99            Value::Str(k) => entries.borrow().contains_key(k.as_ref()),
100            _ => false,
101        },
102        Value::Str(haystack) => match &needle {
103            Value::Str(sub) => haystack.contains(sub.as_ref()),
104            _ => {
105                return Err(DogeError::type_error(format!(
106                    "can only check if a Str is in a Str, not {}",
107                    needle.describe()
108                )));
109            }
110        },
111        Value::Int(_)
112        | Value::Float(_)
113        | Value::Bool(_)
114        | Value::None
115        | Value::Object(_)
116        | Value::Function(_)
117        | Value::Class(_)
118        | Value::BoundMethod(_)
119        | Value::Error(_) => {
120            return Err(DogeError::type_error(format!(
121                "in wants a List, Dict, or Str on the right, not {}",
122                container.describe()
123            )));
124        }
125    };
126    Ok(Value::Bool(found))
127}
128
129/// `needle not in container` — the negation of [`in_`], sharing its type rules so
130/// `x not in xs` and `not (x in xs)` always agree.
131pub fn not_in(needle: Value, container: Value) -> DogeResult {
132    match in_(needle, container)? {
133        Value::Bool(found) => Ok(Value::Bool(!found)),
134        _ => unreachable!("in_ always yields a Bool"),
135    }
136}
137
138/// Ordering for `< <= > >=`: numbers compare across Int/Float, Str compares
139/// lexicographically, anything else is a type error. The list `sort` method
140/// reuses this so its ordering matches the comparison operators exactly.
141pub(crate) fn order(a: &Value, b: &Value) -> DogeResult<Ordering> {
142    if let (Some(x), Some(y)) = (as_f64(a), as_f64(b)) {
143        return x.partial_cmp(&y).ok_or_else(|| {
144            DogeError::type_error(format!(
145                "cannot compare {} and {}",
146                a.describe(),
147                b.describe()
148            ))
149        });
150    }
151    if let (Value::Str(x), Value::Str(y)) = (a, b) {
152        return Ok(x.as_ref().cmp(y.as_ref()));
153    }
154    Err(DogeError::type_error(format!(
155        "cannot compare {} and {}",
156        a.describe(),
157        b.describe()
158    )))
159}
160
161/// `<`.
162pub fn lt(a: Value, b: Value) -> DogeResult {
163    Ok(Value::Bool(order(&a, &b)? == Ordering::Less))
164}
165
166/// `<=`.
167pub fn le(a: Value, b: Value) -> DogeResult {
168    Ok(Value::Bool(matches!(
169        order(&a, &b)?,
170        Ordering::Less | Ordering::Equal
171    )))
172}
173
174/// `>`.
175pub fn gt(a: Value, b: Value) -> DogeResult {
176    Ok(Value::Bool(order(&a, &b)? == Ordering::Greater))
177}
178
179/// `>=`.
180pub fn ge(a: Value, b: Value) -> DogeResult {
181    Ok(Value::Bool(matches!(
182        order(&a, &b)?,
183        Ordering::Greater | Ordering::Equal
184    )))
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn bound_methods_equal_only_on_the_same_receiver_and_name() {
193        let a = Value::object(0, "Shibe");
194        let b = Value::object(0, "Shibe");
195        // Same instance, same method → equal.
196        assert!(values_equal(
197            &Value::bound_method(a.clone(), "speak"),
198            &Value::bound_method(a.clone(), "speak"),
199        ));
200        // Same instance, different method → not equal.
201        assert!(!values_equal(
202            &Value::bound_method(a.clone(), "speak"),
203            &Value::bound_method(a.clone(), "wag"),
204        ));
205        // Different instances of the same class → not equal.
206        assert!(!values_equal(
207            &Value::bound_method(a, "speak"),
208            &Value::bound_method(b, "speak"),
209        ));
210    }
211}