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.type_of() != b.type_of() {
37 return false;
38 }
39 match a.type_of() {
40 Type::Int | Type::Float => number_eq(a, b, Mode::Strict),
41 Type::String => a
42 .as_string()
43 .and_then(|s| s.to_string().ok())
44 .eq(&b.as_string().and_then(|s| s.to_string().ok())),
45 Type::Bool => a.as_bool() == b.as_bool(),
46 Type::Undefined | Type::Null | Type::Uninitialized => true,
47 _ => a == b,
48 }
49}
50
51#[must_use]
53pub fn loose_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
54 if a.type_of() == b.type_of() {
55 return match a.type_of() {
58 Type::Int | Type::Float => number_eq(a, b, Mode::Loose),
59 _ => strict_equal(a, b),
60 };
61 }
62 loose_primitive_eq(a, b)
63}
64
65fn equal_at<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
66 if depth > MAX_DEPTH {
67 return Ok(false);
68 }
69 if a.type_of() != b.type_of() {
70 return Ok(mode == Mode::Loose && loose_primitive_eq(a, b));
72 }
73
74 match a.type_of() {
75 Type::Undefined | Type::Null | Type::Uninitialized => Ok(true),
76 Type::Bool => Ok(a.as_bool() == b.as_bool()),
77 Type::Int | Type::Float => Ok(number_eq(a, b, mode)),
78 Type::String => Ok(js_string(a)? == js_string(b)?),
79 Type::Symbol => Ok(a.as_symbol() == b.as_symbol()),
80 Type::Array => array_eq(a, b, mode, depth),
81 Type::Object | Type::Exception => object_eq(a, b, mode, depth),
82 _ => Ok(a == b),
84 }
85}
86
87fn js_string(value: &Value<'_>) -> rquickjs::Result<String> {
88 value.as_string().map_or_else(|| Ok(String::new()), rquickjs::String::to_string)
89}
90
91fn number_eq(a: &Value<'_>, b: &Value<'_>, mode: Mode) -> bool {
94 let (Some(x), Some(y)) = (a.as_number(), b.as_number()) else {
95 return false;
96 };
97 match mode {
98 Mode::Strict => {
99 if x.is_nan() && y.is_nan() {
100 true
101 } else {
102 x == y && x.is_sign_negative() == y.is_sign_negative()
103 }
104 },
105 Mode::Loose => x == y,
106 }
107}
108
109fn loose_primitive_eq(a: &Value<'_>, b: &Value<'_>) -> bool {
114 let nullish = |v: &Value<'_>| v.is_null() || v.is_undefined();
115 if nullish(a) || nullish(b) {
116 return nullish(a) && nullish(b);
117 }
118 match (coerce_number(a), coerce_number(b)) {
119 (Some(x), Some(y)) => x == y,
120 _ => false,
121 }
122}
123
124fn coerce_number(value: &Value<'_>) -> Option<f64> {
126 match value.type_of() {
127 Type::Int | Type::Float => value.as_number(),
128 Type::Bool => value.as_bool().map(|b| if b { 1.0 } else { 0.0 }),
129 Type::String => {
130 let text = value.as_string()?.to_string().ok()?;
131 let trimmed = text.trim();
132 if trimmed.is_empty() {
133 return Some(0.0);
134 }
135 trimmed.parse::<f64>().ok()
136 },
137 _ => None,
138 }
139}
140
141fn array_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
142 let (Some(x), Some(y)) = (a.as_array(), b.as_array()) else {
143 return Ok(false);
144 };
145 if x.len() != y.len() {
146 return Ok(false);
147 }
148 for i in 0..x.len() {
149 let (lhs, rhs): (Value<'_>, Value<'_>) = (x.get(i)?, y.get(i)?);
150 if !equal_at(&lhs, &rhs, mode, depth + 1)? {
151 return Ok(false);
152 }
153 }
154 Ok(true)
155}
156
157fn object_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
158 let (Some(x), Some(y)) = (a.as_object(), b.as_object()) else {
159 return Ok(false);
160 };
161
162 let (xc, yc) = (constructor_name(x)?, constructor_name(y)?);
167 if mode == Mode::Strict && xc != yc {
168 return Ok(false);
169 }
170
171 if xc.as_deref() == Some("Date") && yc.as_deref() == Some("Date") {
173 let (lhs, rhs) = (value_of_number(x)?, value_of_number(y)?);
174 return Ok(lhs == rhs || (lhs.is_nan() && rhs.is_nan()));
175 }
176 if xc.as_deref() == Some("RegExp") && yc.as_deref() == Some("RegExp") {
177 return Ok(regexp_source(x)? == regexp_source(y)?);
178 }
179
180 let keys: Vec<Atom<'js>> = x.keys::<Atom<'js>>().collect::<rquickjs::Result<Vec<_>>>()?;
185 let other: Vec<Atom<'js>> = y.keys::<Atom<'js>>().collect::<rquickjs::Result<Vec<_>>>()?;
186 if keys.len() != other.len() {
187 return Ok(false);
188 }
189 for key in keys {
190 if !y.contains_key(key.clone())? {
191 return Ok(false);
192 }
193 let (lhs, rhs): (Value<'js>, Value<'js>) = (x.get(key.clone())?, y.get(key)?);
194 if !equal_at(&lhs, &rhs, mode, depth + 1)? {
195 return Ok(false);
196 }
197 }
198 Ok(true)
199}
200
201fn constructor_name(object: &Object<'_>) -> rquickjs::Result<Option<String>> {
202 let ctor: Option<Object<'_>> = object.get(PredefinedAtom::Constructor).ok();
203 match ctor {
204 Some(c) => Ok(c.get::<_, String>(PredefinedAtom::Name).ok()),
205 None => Ok(None),
206 }
207}
208
209fn value_of_number(object: &Object<'_>) -> rquickjs::Result<f64> {
212 let value_of: Function<'_> = object.get(PredefinedAtom::ValueOf)?;
213 value_of.call((This(object.clone()),))
214}
215
216fn regexp_source(object: &Object<'_>) -> rquickjs::Result<String> {
219 let source: String = object.get(PredefinedAtom::Source)?;
220 let flags: String = object.get(PredefinedAtom::Flags).unwrap_or_default();
221 Ok(format!("/{source}/{flags}"))
222}