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 compare by canonical text, matching how citizen fields round trip.
93///
94/// # Examples
95///
96/// ```
97/// # use sim_citizen::{expr_citizen_eq, CitizenField};
98/// let left = 7_i64.encode_field();
99/// let right = 7_i64.encode_field();
100/// assert!(expr_citizen_eq(&left, &right));
101///
102/// let other = 8_i64.encode_field();
103/// assert!(!expr_citizen_eq(&left, &other));
104/// ```
105pub fn expr_citizen_eq(left: &Expr, right: &Expr) -> bool {
106    match (left, right) {
107        (Expr::Number(left), Expr::Number(right)) if left.domain.name.as_ref() == "f64" => {
108            left.canonical == right.canonical
109        }
110        _ => left.canonical_eq(right),
111    }
112}
113
114fn object_encoding_eq(left: &ObjectEncoding, right: &ObjectEncoding) -> bool {
115    match (left, right) {
116        (
117            ObjectEncoding::Constructor {
118                class: left_class,
119                args: left_args,
120            },
121            ObjectEncoding::Constructor {
122                class: right_class,
123                args: right_args,
124            },
125        ) => {
126            left_class == right_class
127                && left_args.len() == right_args.len()
128                && left_args
129                    .iter()
130                    .zip(right_args.iter())
131                    .all(|(left, right)| expr_citizen_eq(left, right))
132        }
133        (
134            ObjectEncoding::TaggedData {
135                tag: left_tag,
136                fields: left_fields,
137            },
138            ObjectEncoding::TaggedData {
139                tag: right_tag,
140                fields: right_fields,
141            },
142        ) => {
143            left_tag == right_tag
144                && left_fields.len() == right_fields.len()
145                && left_fields.iter().zip(right_fields.iter()).all(
146                    |((left_name, left), (right_name, right))| {
147                        left_name == right_name && expr_citizen_eq(left, right)
148                    },
149                )
150        }
151        (
152            ObjectEncoding::Opaque {
153                class: left_class,
154                stable_id: left_id,
155            },
156            ObjectEncoding::Opaque {
157                class: right_class,
158                stable_id: right_id,
159            },
160        ) => left_class == right_class && left_id == right_id,
161        _ => false,
162    }
163}