Skip to main content

ferrijs_std/node/
deep_equal.rs

1//! Structural equality, in Node's two flavours.
2//!
3//! One implementation, shared by `util.isDeepStrictEqual` and by
4//! `assert.deepEqual` / `assert.deepStrictEqual`.
5
6use rquickjs::{Atom, Function, Object, Type, Value, atom::PredefinedAtom, function::This};
7
8/// How leaf values compare.
9#[derive(Clone, Copy, PartialEq, Eq)]
10pub enum Mode {
11  /// `assert.deepEqual`: `==` on primitives, prototypes ignored.
12  Loose,
13  /// `assert.deepStrictEqual`: `Object.is` on primitives, prototypes must
14  /// match.
15  Strict,
16}
17
18/// Recursion ceiling. A cyclic graph would otherwise never terminate;
19/// Node tracks visited pairs instead, which needs identity keys QuickJS
20/// does not hand out cheaply.
21const MAX_DEPTH: usize = 64;
22
23/// Compare two values the way `assert.deepEqual` / `deepStrictEqual` do.
24///
25/// # Errors
26///
27/// Propagates JS-side property reads.
28pub 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/// `===`: `Object.is` on primitives (so `NaN` matches itself and `0` does
33/// not match `-0`), identity on everything else.
34#[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/// `==`: strict equality, plus the primitive coercions.
55#[must_use]
56pub fn loose_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
57  if a.type_of() == b.type_of() {
58    // `==` and `===` differ only across types, except that `NaN` is equal
59    // to nothing under either.
60    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    // `1 == '1'` under the loose flavour; nothing else crosses types.
77    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    // Functions, constructors and everything else compare by identity.
89    _ => 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
97/// `Object.is` semantics under Strict (so `NaN` equals itself and `0` does
98/// not equal `-0`), `==` semantics under Loose.
99fn 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
115/// `==` across types: the numeric coercion JS performs for
116/// number/string/boolean pairs, plus `null == undefined`. Objects are NOT
117/// coerced through `valueOf` / `toString` — Node's `assert.equal` on an
118/// object against a primitive is a comparison nobody writes on purpose.
119fn 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
130/// `ToNumber` for the primitive types `==` coerces.
131fn 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  // Each side's constructor name, once. It used to be recomputed six
169  // times per comparison -- here, and again inside the `Date` and
170  // `RegExp` probes for both operands -- and each call interned two
171  // atoms and allocated a String.
172  let (xc, yc) = (constructor_name(x)?, constructor_name(y)?);
173  if mode == Mode::Strict && xc != yc {
174    return Ok(false);
175  }
176
177  // Dates and RegExps carry their state outside their own properties.
178  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  // Keys as atoms, not as `String`s. A property name that arrives as a
187  // Rust string has to be interned again for every lookup it is used
188  // in -- and this loop used it in three -- on top of the C-string
189  // round trip that built it.
190  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
215/// `valueOf()` for a `Date`, whose identity is a number rather than its
216/// own properties. The caller has already established the constructor.
217fn 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
222/// A `RegExp`'s pattern and flags, which is its whole identity. The
223/// caller has already established the constructor.
224fn 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}