1use rquickjs::{Atom, Function, Object, Type, Value, atom::PredefinedAtom, function::This};
7
8#[derive(Clone, Copy, PartialEq, Eq)]
10pub enum Mode {
11 Loose,
13 Strict,
16}
17
18const MAX_DEPTH: usize = 64;
22
23pub fn deep_equal<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode) -> rquickjs::Result<bool> {
29 equal_at(a, b, mode, 0)
30}
31
32#[must_use]
35pub fn strict_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
36 if a.is_number() && b.is_number() {
37 return number_eq(a, b, Mode::Strict);
38 }
39 if a.type_of() != b.type_of() {
40 return false;
41 }
42 match a.type_of() {
43 Type::Int | Type::Float => number_eq(a, b, Mode::Strict),
44 Type::String => a
45 .as_string()
46 .and_then(|s| s.to_string().ok())
47 .eq(&b.as_string().and_then(|s| s.to_string().ok())),
48 Type::Bool => a.as_bool() == b.as_bool(),
49 Type::Undefined | Type::Null | Type::Uninitialized => true,
50 _ => a == b,
51 }
52}
53
54#[must_use]
56pub fn loose_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
57 if a.type_of() == b.type_of() {
58 return match a.type_of() {
61 Type::Int | Type::Float => number_eq(a, b, Mode::Loose),
62 _ => strict_equal(a, b),
63 };
64 }
65 loose_primitive_eq(a, b)
66}
67
68fn equal_at<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
69 if depth > MAX_DEPTH {
70 return Ok(false);
71 }
72 if a.is_number() && b.is_number() {
73 return Ok(number_eq(a, b, mode));
74 }
75 if a.type_of() != b.type_of() {
76 return Ok(mode == Mode::Loose && loose_primitive_eq(a, b));
78 }
79
80 match a.type_of() {
81 Type::Undefined | Type::Null | Type::Uninitialized => Ok(true),
82 Type::Bool => Ok(a.as_bool() == b.as_bool()),
83 Type::Int | Type::Float => Ok(number_eq(a, b, mode)),
84 Type::String => Ok(js_string(a)? == js_string(b)?),
85 Type::Symbol => Ok(a.as_symbol() == b.as_symbol()),
86 Type::Array => array_eq(a, b, mode, depth),
87 Type::Object | Type::Exception => object_eq(a, b, mode, depth),
88 _ => Ok(a == b),
90 }
91}
92
93fn js_string(value: &Value<'_>) -> rquickjs::Result<String> {
94 value.as_string().map_or_else(|| Ok(String::new()), rquickjs::String::to_string)
95}
96
97fn number_eq(a: &Value<'_>, b: &Value<'_>, mode: Mode) -> bool {
100 let (Some(x), Some(y)) = (a.as_number(), b.as_number()) else {
101 return false;
102 };
103 match mode {
104 Mode::Strict => {
105 if x.is_nan() && y.is_nan() {
106 true
107 } else {
108 x == y && x.is_sign_negative() == y.is_sign_negative()
109 }
110 },
111 Mode::Loose => x == y,
112 }
113}
114
115fn loose_primitive_eq(a: &Value<'_>, b: &Value<'_>) -> bool {
120 let nullish = |v: &Value<'_>| v.is_null() || v.is_undefined();
121 if nullish(a) || nullish(b) {
122 return nullish(a) && nullish(b);
123 }
124 match (coerce_number(a), coerce_number(b)) {
125 (Some(x), Some(y)) => x == y,
126 _ => false,
127 }
128}
129
130fn coerce_number(value: &Value<'_>) -> Option<f64> {
132 match value.type_of() {
133 Type::Int | Type::Float => value.as_number(),
134 Type::Bool => value.as_bool().map(|b| if b { 1.0 } else { 0.0 }),
135 Type::String => {
136 let text = value.as_string()?.to_string().ok()?;
137 let trimmed = text.trim();
138 if trimmed.is_empty() {
139 return Some(0.0);
140 }
141 trimmed.parse::<f64>().ok()
142 },
143 _ => None,
144 }
145}
146
147fn array_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
148 let (Some(x), Some(y)) = (a.as_array(), b.as_array()) else {
149 return Ok(false);
150 };
151 if x.len() != y.len() {
152 return Ok(false);
153 }
154 for i in 0..x.len() {
155 let (lhs, rhs): (Value<'_>, Value<'_>) = (x.get(i)?, y.get(i)?);
156 if !equal_at(&lhs, &rhs, mode, depth + 1)? {
157 return Ok(false);
158 }
159 }
160 Ok(true)
161}
162
163fn object_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
164 let (Some(x), Some(y)) = (a.as_object(), b.as_object()) else {
165 return Ok(false);
166 };
167
168 let (xc, yc) = (constructor_name(x)?, constructor_name(y)?);
173 if mode == Mode::Strict && xc != yc {
174 return Ok(false);
175 }
176
177 if xc.as_deref() == Some("Date") && yc.as_deref() == Some("Date") {
179 let (lhs, rhs) = (value_of_number(x)?, value_of_number(y)?);
180 return Ok(lhs == rhs || (lhs.is_nan() && rhs.is_nan()));
181 }
182 if xc.as_deref() == Some("RegExp") && yc.as_deref() == Some("RegExp") {
183 return Ok(regexp_source(x)? == regexp_source(y)?);
184 }
185
186 let keys: Vec<Atom<'js>> = x.keys::<Atom<'js>>().collect::<rquickjs::Result<Vec<_>>>()?;
191 let other: Vec<Atom<'js>> = y.keys::<Atom<'js>>().collect::<rquickjs::Result<Vec<_>>>()?;
192 if keys.len() != other.len() {
193 return Ok(false);
194 }
195 for key in keys {
196 if !y.contains_key(key.clone())? {
197 return Ok(false);
198 }
199 let (lhs, rhs): (Value<'js>, Value<'js>) = (x.get(key.clone())?, y.get(key)?);
200 if !equal_at(&lhs, &rhs, mode, depth + 1)? {
201 return Ok(false);
202 }
203 }
204 Ok(true)
205}
206
207fn constructor_name(object: &Object<'_>) -> rquickjs::Result<Option<String>> {
208 let ctor: Option<Object<'_>> = object.get(PredefinedAtom::Constructor).ok();
209 match ctor {
210 Some(c) => Ok(c.get::<_, String>(PredefinedAtom::Name).ok()),
211 None => Ok(None),
212 }
213}
214
215fn value_of_number(object: &Object<'_>) -> rquickjs::Result<f64> {
218 let value_of: Function<'_> = object.get(PredefinedAtom::ValueOf)?;
219 value_of.call((This(object.clone()),))
220}
221
222fn regexp_source(object: &Object<'_>) -> rquickjs::Result<String> {
225 let source: String = object.get(PredefinedAtom::Source)?;
226 let flags: String = object.get(PredefinedAtom::Flags).unwrap_or_default();
227 Ok(format!("/{source}/{flags}"))
228}