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.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/// `==`: strict equality, plus the primitive coercions.
52#[must_use]
53pub fn loose_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
54  if a.type_of() == b.type_of() {
55    // `==` and `===` differ only across types, except that `NaN` is equal
56    // to nothing under either.
57    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    // `1 == '1'` under the loose flavour; nothing else crosses types.
71    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    // Functions, constructors and everything else compare by identity.
83    _ => 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
91/// `Object.is` semantics under Strict (so `NaN` equals itself and `0` does
92/// not equal `-0`), `==` semantics under Loose.
93fn 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
109/// `==` across types: the numeric coercion JS performs for
110/// number/string/boolean pairs, plus `null == undefined`. Objects are NOT
111/// coerced through `valueOf` / `toString` — Node's `assert.equal` on an
112/// object against a primitive is a comparison nobody writes on purpose.
113fn 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
124/// `ToNumber` for the primitive types `==` coerces.
125fn 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  // Each side's constructor name, once. It used to be recomputed six
163  // times per comparison -- here, and again inside the `Date` and
164  // `RegExp` probes for both operands -- and each call interned two
165  // atoms and allocated a String.
166  let (xc, yc) = (constructor_name(x)?, constructor_name(y)?);
167  if mode == Mode::Strict && xc != yc {
168    return Ok(false);
169  }
170
171  // Dates and RegExps carry their state outside their own properties.
172  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  // Keys as atoms, not as `String`s. A property name that arrives as a
181  // Rust string has to be interned again for every lookup it is used
182  // in -- and this loop used it in three -- on top of the C-string
183  // round trip that built it.
184  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
209/// `valueOf()` for a `Date`, whose identity is a number rather than its
210/// own properties. The caller has already established the constructor.
211fn 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
216/// A `RegExp`'s pattern and flags, which is its whole identity. The
217/// caller has already established the constructor.
218fn 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}