Skip to main content

sim_citizen/
eq.rs

1//! The CitizenEq semantic-equality helper used by the strict citizen gate.
2
3use sim_kernel::{Cx, Expr, ObjectEncoding, Result, Value};
4
5/// Semantic equality between citizen field values for the strict gate.
6///
7/// Implementations compare values the way the citizen round trip does (for
8/// example, `f64` compares by canonical text), so the conformance gate accepts
9/// a decoded value as equal to its original even where derived `PartialEq`
10/// would be too strict or too loose.
11pub trait CitizenEq<Rhs = Self> {
12    /// Returns whether `self` and `rhs` are citizen-equal.
13    fn citizen_eq(&self, rhs: &Rhs) -> bool;
14}
15
16macro_rules! citizen_eq_partial {
17    ($($ty:ty),* $(,)?) => {
18        $(impl CitizenEq for $ty {
19            fn citizen_eq(&self, rhs: &Self) -> bool {
20                self == rhs
21            }
22        })*
23    };
24}
25
26citizen_eq_partial!(
27    bool, String, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, usize
28);
29
30impl CitizenEq for f64 {
31    fn citizen_eq(&self, rhs: &Self) -> bool {
32        self.to_string() == rhs.to_string()
33    }
34}
35
36impl<T> CitizenEq for Vec<T>
37where
38    T: CitizenEq,
39{
40    fn citizen_eq(&self, rhs: &Self) -> bool {
41        self.len() == rhs.len()
42            && self
43                .iter()
44                .zip(rhs.iter())
45                .all(|(left, right)| left.citizen_eq(right))
46    }
47}
48
49impl<T> CitizenEq for Option<T>
50where
51    T: CitizenEq,
52{
53    fn citizen_eq(&self, rhs: &Self) -> bool {
54        match (self, rhs) {
55            (Some(left), Some(right)) => left.citizen_eq(right),
56            (None, None) => true,
57            _ => false,
58        }
59    }
60}
61
62/// Compares two citizen [`Value`]s for semantic equality.
63///
64/// When both values expose an object encoder it compares their
65/// [`ObjectEncoding`]s; otherwise it falls back to comparing their `Expr`
66/// projections through [`expr_citizen_eq`]. The kernel owns `Value` and the
67/// encoder/`as_expr` surface; this helper applies the citizen equality rule.
68pub fn values_citizen_eq(cx: &mut Cx, left: &Value, right: &Value) -> Result<bool> {
69    let left_encoding = left
70        .object()
71        .as_object_encoder()
72        .map(|encoder| encoder.object_encoding(cx))
73        .transpose()?;
74    let right_encoding = right
75        .object()
76        .as_object_encoder()
77        .map(|encoder| encoder.object_encoding(cx))
78        .transpose()?;
79
80    match (left_encoding, right_encoding) {
81        (Some(left), Some(right)) => Ok(object_encoding_eq(&left, &right)),
82        _ => Ok(expr_citizen_eq(
83            &left.object().as_expr(cx)?,
84            &right.object().as_expr(cx)?,
85        )),
86    }
87}
88
89/// Compares two `Expr`s under citizen equality.
90///
91/// Equivalent to the kernel's canonical equality except that `f64`-domain
92/// numbers only compare by canonical text when both sides are the exact
93/// `numbers/f64` domain, matching how citizen fields round trip while still
94/// failing closed on domain mismatches.
95///
96/// # Examples
97///
98/// ```
99/// # use sim_citizen::{expr_citizen_eq, CitizenField};
100/// let left = 7_i64.encode_field();
101/// let right = 7_i64.encode_field();
102/// assert!(expr_citizen_eq(&left, &right));
103///
104/// let other = 8_i64.encode_field();
105/// assert!(!expr_citizen_eq(&left, &other));
106/// ```
107pub fn expr_citizen_eq(left: &Expr, right: &Expr) -> bool {
108    match (left, right) {
109        (Expr::Number(left), Expr::Number(right))
110            if is_f64_domain(&left.domain) || is_f64_domain(&right.domain) =>
111        {
112            left.domain == right.domain && left.canonical == right.canonical
113        }
114        _ => left.canonical_eq(right),
115    }
116}
117
118fn is_f64_domain(symbol: &sim_kernel::Symbol) -> bool {
119    *symbol == sim_kernel::Symbol::qualified("numbers", "f64")
120}
121
122fn object_encoding_eq(left: &ObjectEncoding, right: &ObjectEncoding) -> bool {
123    match (left, right) {
124        (
125            ObjectEncoding::Constructor {
126                class: left_class,
127                args: left_args,
128            },
129            ObjectEncoding::Constructor {
130                class: right_class,
131                args: right_args,
132            },
133        ) => {
134            left_class == right_class
135                && left_args.len() == right_args.len()
136                && left_args
137                    .iter()
138                    .zip(right_args.iter())
139                    .all(|(left, right)| expr_citizen_eq(left, right))
140        }
141        (
142            ObjectEncoding::TaggedData {
143                tag: left_tag,
144                fields: left_fields,
145            },
146            ObjectEncoding::TaggedData {
147                tag: right_tag,
148                fields: right_fields,
149            },
150        ) => {
151            left_tag == right_tag
152                && left_fields.len() == right_fields.len()
153                && left_fields.iter().zip(right_fields.iter()).all(
154                    |((left_name, left), (right_name, right))| {
155                        left_name == right_name && expr_citizen_eq(left, right)
156                    },
157                )
158        }
159        (
160            ObjectEncoding::Opaque {
161                class: left_class,
162                stable_id: left_id,
163            },
164            ObjectEncoding::Opaque {
165                class: right_class,
166                stable_id: right_id,
167            },
168        ) => left_class == right_class && left_id == right_id,
169        _ => false,
170    }
171}