Skip to main content

nodejs/stdlib/
assert.rs

1//! Node `assert` module. Failing assertions throw an `AssertionError` (returned
2//! as an `Err`, which the host surfaces as a thrown JS exception).
3
4use crate::host::{
5    call_method, invoke, is_callable, promise_of, reject_promise_val, resolve_promise_val,
6    subscribe_native, take_exc_or_error, with_host, JsObj, PromiseState,
7};
8use fusevm::Value;
9
10pub const METHODS: &[&str] = &[
11    "ok",
12    "equal",
13    "notEqual",
14    "strictEqual",
15    "notStrictEqual",
16    "deepEqual",
17    "notDeepEqual",
18    "deepStrictEqual",
19    "notDeepStrictEqual",
20    "throws",
21    "doesNotThrow",
22    "fail",
23    "match",
24    "doesNotMatch",
25    "ifError",
26    "partialDeepStrictEqual",
27    "rejects",
28    "doesNotReject",
29];
30
31pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
32    let a = || args.first().cloned().unwrap_or(Value::Undef);
33    let b = || args.get(1).cloned().unwrap_or(Value::Undef);
34    Some(match method {
35        "ok" => assert_ok(args),
36        "equal" => check(loose_eq(&a(), &b()), args, 2, "==", &a(), &b()),
37        "notEqual" => check(!loose_eq(&a(), &b()), args, 2, "!=", &a(), &b()),
38        "strictEqual" => check(strict(&a(), &b()), args, 2, "===", &a(), &b()),
39        "notStrictEqual" => check(!strict(&a(), &b()), args, 2, "!==", &a(), &b()),
40        "deepEqual" => check(
41            deep_equal(&a(), &b(), false),
42            args,
43            2,
44            "deepEqual",
45            &a(),
46            &b(),
47        ),
48        "notDeepEqual" => check(
49            !deep_equal(&a(), &b(), false),
50            args,
51            2,
52            "notDeepEqual",
53            &a(),
54            &b(),
55        ),
56        "deepStrictEqual" => check(
57            deep_equal(&a(), &b(), true),
58            args,
59            2,
60            "deepStrictEqual",
61            &a(),
62            &b(),
63        ),
64        "notDeepStrictEqual" => check(
65            !deep_equal(&a(), &b(), true),
66            args,
67            2,
68            "notDeepStrictEqual",
69            &a(),
70            &b(),
71        ),
72        "throws" => throws(args, true),
73        "doesNotThrow" => throws(args, false),
74        // `fail` carries no operands: `actual`/`expected` are own properties
75        // holding `undefined`, and `operator` is the literal `"fail"`.
76        "fail" => Err(throw_assertion(
77            &message(args, 0).unwrap_or_else(|| "Failed".to_string()),
78            message(args, 0).is_none(),
79            "fail",
80            Value::Undef,
81            Value::Undef,
82        )),
83        "match" => assert_match(args, true),
84        "doesNotMatch" => assert_match(args, false),
85        "ifError" => if_error(&a()),
86        "partialDeepStrictEqual" => partial(&a(), &b(), args),
87        "rejects" => Ok(rejects_impl(&a(), true)),
88        "doesNotReject" => Ok(rejects_impl(&a(), false)),
89        _ => return None,
90    })
91}
92
93/// The strict-mode variants (`assert.strict.equal` === `assert.strictEqual`).
94/// Maps the loose method names onto their strict counterparts, then delegates.
95pub fn strict_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
96    let mapped = match method {
97        "equal" => "strictEqual",
98        "notEqual" => "notStrictEqual",
99        "deepEqual" => "deepStrictEqual",
100        "notDeepEqual" => "notDeepStrictEqual",
101        other => other,
102    };
103    call(mapped, args)
104}
105
106/// `assert.match(string, regexp)` / `assert.doesNotMatch(...)`. `regexp` must be
107/// a `RegExp`; matching runs through the JS `RegExp.prototype.test`.
108fn assert_match(args: &[Value], want_match: bool) -> Result<Value, String> {
109    let s = args.first().cloned().unwrap_or(Value::Undef);
110    let re = args.get(1).cloned().unwrap_or(Value::Undef);
111    if !with_host(|h| matches!(h.get(&re), Some(JsObj::RegExp(_)))) {
112        // Node names the instance, not a type, and appends the received value.
113        return Err(crate::host::coded_error(
114            "TypeError",
115            "ERR_INVALID_ARG_TYPE",
116            &format!(
117                "The \"regexp\" argument must be an instance of RegExp. Received {}",
118                crate::stdlib::received_desc(&re)
119            ),
120        ));
121    }
122    let matched = call_method(&re, "test", vec![s.clone()])?;
123    let matched = with_host(|h| h.truthy(&matched));
124    if matched == want_match {
125        return Ok(Value::Undef);
126    }
127    if let Some(m) = message(args, 2) {
128        return Err(assertion_error(&m));
129    }
130    let (sre, sstr) = with_host(|h| (h.inspect(&re), h.str_of(&s)));
131    let verb = if want_match {
132        "The input did not match the regular expression"
133    } else {
134        "The input was expected to not match the regular expression"
135    };
136    Err(assertion_error(&format!("{verb} {sre}. Input: '{sstr}'")))
137}
138
139/// `assert.ifError(value)` — throws unless `value` is `null`/`undefined`.
140fn if_error(v: &Value) -> Result<Value, String> {
141    if with_host(|h| h.is_nullish(v)) {
142        return Ok(Value::Undef);
143    }
144    let desc = with_host(|h| match h.get(v) {
145        Some(JsObj::Object(p)) => p
146            .get("message")
147            .map(|m| h.str_of(m))
148            .unwrap_or_else(|| h.inspect(v)),
149        _ => h.inspect(v),
150    });
151    Err(assertion_error(&format!(
152        "ifError got unwanted exception: {desc}"
153    )))
154}
155
156/// `assert.partialDeepStrictEqual(actual, expected)` — passes when every leaf of
157/// `expected` strict-deep-matches the corresponding part of `actual` (extra
158/// props/elements in `actual` are ignored).
159fn partial(actual: &Value, expected: &Value, args: &[Value]) -> Result<Value, String> {
160    if partial_deep(actual, expected) {
161        return Ok(Value::Undef);
162    }
163    if let Some(m) = message(args, 2) {
164        return Err(assertion_error(&m));
165    }
166    let (sa, sb) = with_host(|h| (h.inspect(actual), h.inspect(expected)));
167    Err(assertion_error(&format!(
168        "Expected values to be strictly deep-equal (partial):\n{sb} should be a subset of {sa}"
169    )))
170}
171
172fn partial_deep(actual: &Value, expected: &Value) -> bool {
173    let ekind = with_host(|h| h.get(expected).map(kind));
174    match ekind {
175        Some(Kind::Object) => {
176            if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Object)) {
177                return false;
178            }
179            let (ea, ee) = with_host(|h| (object_of(h, actual), object_of(h, expected)));
180            ee.iter().all(|(k, ve)| {
181                ea.iter()
182                    .find(|(k2, _)| k2 == k)
183                    .is_some_and(|(_, va)| partial_deep(va, ve))
184            })
185        }
186        Some(Kind::Array) => {
187            if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Array)) {
188                return false;
189            }
190            let (ia, ie) = with_host(|h| (array_of(h, actual), array_of(h, expected)));
191            ie.len() <= ia.len() && ie.iter().zip(ia.iter()).all(|(e, a)| partial_deep(a, e))
192        }
193        _ => strict(actual, expected),
194    }
195}
196
197/// `assert.rejects(fn|promise)` / `assert.doesNotReject(...)` — returns a Promise
198/// that fulfills when the operand settles the expected way, else rejects with an
199/// `AssertionError`.
200fn rejects_impl(input: &Value, want_reject: bool) -> Value {
201    let result = with_host(|h| h.new_promise());
202    let rid = with_host(|h| h.promise_id(&result).unwrap());
203    // Reduce the operand to a promise: call it if it is a function.
204    let operand = if with_host(|h| is_callable(h, input)) {
205        match invoke(input, Vec::new(), None) {
206            Ok(v) => promise_of(&v),
207            Err(e) => {
208                let ev = take_exc_or_error(&e);
209                let p = with_host(|h| h.new_promise());
210                let pid = with_host(|h| h.promise_id(&p).unwrap());
211                reject_promise_val(pid, ev);
212                p
213            }
214        }
215    } else {
216        promise_of(input)
217    };
218    let Some(oid) = with_host(|h| h.promise_id(&operand)) else {
219        // Not thenable: treat as an immediate non-rejection.
220        settle_rejects(rid, false, want_reject);
221        return result;
222    };
223    subscribe_native(
224        oid,
225        Box::new(move |state, _val| {
226            settle_rejects(rid, state == PromiseState::Rejected, want_reject);
227            Ok(())
228        }),
229    );
230    result
231}
232
233/// `new assert.AssertionError(options)` — a real `Error`-prototype-linked object
234/// carrying `name`/`message`/`code`/`actual`/`expected`/`operator`. Parent wires
235/// this to `construct("AssertionError")` and `constant("assert","AssertionError")`.
236pub fn construct_assertion_error(args: &[Value]) -> Value {
237    let opts = args.first().cloned().unwrap_or(Value::Undef);
238    let (message, actual, expected, operator) = with_host(|h| match h.get(&opts) {
239        Some(JsObj::Object(p)) => (
240            p.get("message").map(|v| h.str_of(v)),
241            p.get("actual").cloned(),
242            p.get("expected").cloned(),
243            p.get("operator").map(|v| h.str_of(v)),
244        ),
245        _ => (None, None, None, None),
246    });
247    let generated = message.is_none();
248    let msg = message.unwrap_or_else(|| {
249        let (sa, se) = with_host(|h| {
250            (
251                actual.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
252                expected.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
253            )
254        });
255        let op = operator.clone().unwrap_or_else(|| "==".to_string());
256        format!("{sa} {op} {se}")
257    });
258    assertion_error_object(
259        &msg,
260        generated,
261        operator.as_deref(),
262        actual.unwrap_or(Value::Undef),
263        expected.unwrap_or(Value::Undef),
264    )
265}
266
267/// Node's `diff` field. Every failure form measured on node v26.7.0 — `ok`,
268/// `equal`, `strictEqual`, `notStrictEqual`, `deepEqual`, `deepStrictEqual`,
269/// `match`, `throws`, `fail` — reports the same `"simple"`; it names the diff
270/// MODE the error was built under, not a rendered diff.
271const DIFF_MODE: &str = "simple";
272
273/// Build the `AssertionError` object a failing assertion throws.
274///
275/// The own-property set is what a test runner reads, and node-js carried only
276/// `code`/`message`/`stack`: `err.actual`, `err.expected`, `err.operator` and
277/// `err.generatedMessage` were all `undefined`, so every framework that reports
278/// "expected X, got Y" from a caught `AssertionError` had nothing to report.
279/// Measured on node v26.7.0, `Object.keys(err)` is
280/// `["generatedMessage","code","actual","expected","operator","diff"]` — in that
281/// order — while `name`, `message` and `stack` are own but NOT enumerable.
282fn assertion_error_object(
283    msg: &str,
284    generated: bool,
285    operator: Option<&str>,
286    actual: Value,
287    expected: Value,
288) -> Value {
289    let stack = format!("AssertionError [ERR_ASSERTION]: {msg}\n    at <anonymous>");
290    let op_val = match operator {
291        Some(o) => with_host(|h| h.new_str(o)),
292        None => Value::Undef,
293    };
294    let name_v = with_host(|h| h.new_str("AssertionError"));
295    let msg_v = with_host(|h| h.new_str(msg));
296    let code_v = with_host(|h| h.new_str("ERR_ASSERTION"));
297    let stack_v = with_host(|h| h.new_str(stack));
298    let diff_v = with_host(|h| h.new_str(DIFF_MODE));
299    let mut props: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
300    // Enumerable, in node's order, first.
301    props.insert("generatedMessage".into(), Value::Bool(generated));
302    props.insert("code".into(), code_v);
303    props.insert("actual".into(), actual);
304    props.insert("expected".into(), expected);
305    props.insert("operator".into(), op_val);
306    props.insert("diff".into(), diff_v);
307    props.insert("name".into(), name_v);
308    props.insert("message".into(), msg_v);
309    props.insert("stack".into(), stack_v);
310    let obj = with_host(|h| h.new_object(props));
311    with_host(|h| {
312        for k in ["name", "message", "stack"] {
313            h.hide_prop(&obj, k);
314        }
315        h.ensure_error_protos();
316        // The `AssertionError` prototype, not `Error`'s: `e.constructor.name`
317        // is what a test runner branches on, and linking straight to `Error`
318        // reported `Error` there while `e.name` still said `AssertionError`.
319        if let Some(p) = crate::host::error_proto_of(h, "AssertionError") {
320            h.set_proto(&obj, p);
321        }
322    });
323    obj
324}
325
326/// Raise a failing assertion as a REAL `AssertionError` object.
327///
328/// The internal `Name [CODE]: message` string is still what propagates (it is
329/// what an uncaught failure prints), but the live thrown VALUE is parked in
330/// `host.exc` so a `catch` receives the object with its full property set rather
331/// than one synthesized from the message alone.
332fn throw_assertion(
333    msg: &str,
334    generated: bool,
335    operator: &str,
336    actual: Value,
337    expected: Value,
338) -> String {
339    let err = assertion_error_object(msg, generated, Some(operator), actual, expected);
340    with_host(|h| h.exc = Some(err));
341    assertion_error(msg)
342}
343
344fn settle_rejects(rid: u32, rejected: bool, want_reject: bool) {
345    if rejected == want_reject {
346        resolve_promise_val(rid, Value::Undef);
347    } else {
348        let msg = if want_reject {
349            "AssertionError [ERR_ASSERTION]: Missing expected rejection."
350        } else {
351            "AssertionError [ERR_ASSERTION]: Got unwanted rejection."
352        };
353        let ev = with_host(|h| crate::builtins::synth_error(h, msg));
354        reject_promise_val(rid, ev);
355    }
356}
357
358/// `assert(value[, message])` — throws unless `value` is truthy.
359pub fn assert_ok(args: &[Value]) -> Result<Value, String> {
360    let v = args.first().cloned().unwrap_or(Value::Undef);
361    if with_host(|h| h.truthy(&v)) {
362        return Ok(Value::Undef);
363    }
364    let custom = message(args, 1);
365    let msg = custom.clone().unwrap_or_else(||
366        // Node's heading ends with a colon and is followed by an echo of
367        // the failing source line, which needs the call site's text.
368        "The expression evaluated to a falsy value:".to_string());
369    // `ok` reports the operand as `actual` against a literal `true`, under the
370    // `==` operator (measured on node v26.7.0: `assert.ok(0)` gives
371    // `actual: 0`, `expected: true`, `operator: '=='`).
372    Err(throw_assertion(
373        &msg,
374        custom.is_none(),
375        "==",
376        v,
377        Value::Bool(true),
378    ))
379}
380
381fn check(
382    pass: bool,
383    args: &[Value],
384    msg_idx: usize,
385    op: &str,
386    a: &Value,
387    b: &Value,
388) -> Result<Value, String> {
389    if pass {
390        return Ok(Value::Undef);
391    }
392    let custom = message(args, msg_idx);
393    // `strictEqual`, `deepStrictEqual` and `partialDeepStrictEqual` are Node's
394    // `kMethodsWithCustomMessageDiff`: they render a structural `+ actual -
395    // expected` diff of the two operands, and they keep rendering it when a
396    // custom message is supplied (the custom text replaces the heading only).
397    // Every other comparison writes a fixed sentence.
398    let diff_operator = match op {
399        "===" => Some("strictEqual"),
400        "deepStrictEqual" => Some("deepStrictEqual"),
401        "partialDeepStrictEqual" => Some("partialDeepStrictEqual"),
402        _ => None,
403    };
404    if let Some(diff_op) = diff_operator {
405        let msg = super::assert_diff::create_err_diff(a, b, diff_op, custom.as_deref());
406        let operator = if op == "===" { "strictEqual" } else { op };
407        return Err(throw_assertion(
408            &msg,
409            custom.is_none(),
410            operator,
411            a.clone(),
412            b.clone(),
413        ));
414    }
415    // The remaining messages echo one or both operands, and do so with assert's
416    // OWN inspect settings (expanded and sorted), not `console.log`'s: node
417    // reports `notDeepStrictEqual({a:1},{a:1})` as `{\n  a: 1\n}`, one property
418    // per line, where the default rendering is `{ a: 1 }`.
419    let (sa, sb) = (
420        super::assert_diff::inspect_operand(a),
421        super::assert_diff::inspect_operand(b),
422    );
423    // Each comparison has its OWN generated-message shape in Node; `{a} {op} {b}`
424    // is only right for the two loose forms. `strictEqual(1, 2)` produced
425    // `1 strictEqual 2` here, which is not a sentence any Node emits — the
426    // operator name was being substituted where Node writes a whole heading.
427    let msg = match op {
428        "==" | "!=" => format!("{sa} {op} {sb}"),
429        "===" => format!("Expected values to be strictly equal:\n\n{sa} !== {sb}\n"),
430        "!==" => format!("Expected \"actual\" to be strictly unequal to: {sa}"),
431        "deepEqual" => format!(
432            "Expected values to be loosely deep-equal:\n\n{sa}\n\nshould loosely \
433             deep-equal\n\n{sb}"
434        ),
435        "notDeepEqual" => {
436            format!("Expected \"actual\" not to be loosely deep-equal to:\n\n{sa}")
437        }
438        // The `+ actual - expected` structural DIFF Node renders between two
439        // non-primitive operands is not reproduced (it needs a line-oriented
440        // differ over `util.inspect` output); the primitive form, which is the
441        // whole message when neither side is an object, is exact.
442        "deepStrictEqual" => {
443            format!("Expected values to be strictly deep-equal:\n\n{sa} !== {sb}\n")
444        }
445        "notDeepStrictEqual" => {
446            format!("Expected \"actual\" not to be strictly deep-equal to:\n\n{sa}\n")
447        }
448        _ => format!("{sa} {op} {sb}"),
449    };
450    // Node names the METHOD for the strict/deep forms and the OPERATOR for the
451    // two loose ones: `strictEqual` reports `operator: 'strictEqual'` while
452    // `equal` reports `'=='`. A custom message replaces the generated text but
453    // keeps every other field, and flips `generatedMessage` to false.
454    let operator = match op {
455        "===" => "strictEqual",
456        "!==" => "notStrictEqual",
457        other => other,
458    };
459    Err(throw_assertion(
460        &custom.clone().unwrap_or(msg),
461        custom.is_none(),
462        operator,
463        a.clone(),
464        b.clone(),
465    ))
466}
467
468fn throws(args: &[Value], want_throw: bool) -> Result<Value, String> {
469    let f = args.first().cloned().unwrap_or(Value::Undef);
470    // The thrown value becomes `err.actual` on a `doesNotThrow` failure, so it
471    // has to be captured rather than discarded with `.is_err()`.
472    let caught = match invoke(&f, Vec::new(), None) {
473        Ok(_) => None,
474        Err(e) => Some(crate::host::take_exc_or_error(&e)),
475    };
476    let threw = caught.is_some();
477    match (threw, want_throw) {
478        (true, true) | (false, false) => Ok(Value::Undef),
479        // `generatedMessage` is FALSE for both, which is what node reports even
480        // though it wrote the sentence itself (v26.7.0, `assert.throws(()=>{})`).
481        (false, true) => Err(throw_assertion(
482            "Missing expected exception.",
483            false,
484            "throws",
485            Value::Undef,
486            Value::Undef,
487        )),
488        (true, false) => Err(throw_assertion(
489            "Got unwanted exception.",
490            false,
491            "doesNotThrow",
492            caught.unwrap_or(Value::Undef),
493            Value::Undef,
494        )),
495    }
496}
497
498fn message(args: &[Value], idx: usize) -> Option<String> {
499    match args.get(idx) {
500        Some(Value::Undef) | None => None,
501        Some(v) => Some(with_host(|h| h.str_of(v))),
502    }
503}
504
505/// An `AssertionError` as an internal error string.
506///
507/// `Name [CODE]: message` is the shared encoding `synth_error` parses back into
508/// `.name`/`.code`/`.message`, so the prefix must be built by the one
509/// constructor rather than written out here — spelling it inline is what let
510/// this site keep a form the parser did not recognize.
511fn assertion_error(msg: &str) -> String {
512    crate::host::coded_error("AssertionError", "ERR_ASSERTION", msg)
513}
514
515/// `assert.strictEqual` compares with `Object.is`, not `===`.
516///
517/// That is the whole difference for two values: `NaN` equals itself, and `+0`
518/// does not equal `-0`. Using `===` had both backwards — `strictEqual(NaN, NaN)`
519/// failed and `strictEqual(0, -0)` passed.
520fn strict(a: &Value, b: &Value) -> bool {
521    crate::builtins::same_value(a, b)
522}
523
524fn loose_eq(a: &Value, b: &Value) -> bool {
525    if strict(a, b) {
526        return true;
527    }
528    with_host(|h| {
529        let (na, nb) = (h.to_number(a), h.to_number(b));
530        if !na.is_nan() && !nb.is_nan() && (na == nb) {
531            return true;
532        }
533        h.str_of(a) == h.str_of(b)
534    })
535}
536
537/// Structural equality. `strict` compares leaves with `===`, otherwise `==`.
538pub fn deep_equal(a: &Value, b: &Value, strict_mode: bool) -> bool {
539    deep_equal_seen(a, b, strict_mode, &mut Vec::new())
540}
541
542/// `deep_equal` carrying the pairs currently being compared.
543///
544/// Without it a self-referential structure recursed until the stack overflowed
545/// and the process aborted — `const x = {}; x.self = x;` compared against
546/// another of the same shape, which is exactly what a test asserting on a
547/// linked structure does. A pair already on the stack is treated as equal: if
548/// anything else about the two differs, some other comparison finds it.
549fn deep_equal_seen(
550    a: &Value,
551    b: &Value,
552    strict_mode: bool,
553    seen: &mut Vec<(Value, Value)>,
554) -> bool {
555    if seen.iter().any(|(x, y)| x == a && y == b) {
556        return true;
557    }
558    // `deepStrictEqual` requires the two to share a [[Prototype]]. That single
559    // check is what separates `Object.create(null)` from `{}`, an instance of
560    // one class from an instance of another, and a `Uint8Array` from an
561    // `Int8Array` — none of which were being distinguished.
562    if strict_mode {
563        let both_objects = with_host(|h| h.get(a).is_some() && h.get(b).is_some());
564        if both_objects
565            && with_host(|h| {
566                // A null-prototype object is tracked separately rather than by
567                // `proto_of` returning None — which a plain object does too, its
568                // `Object.prototype` being implicit. Comparing only `proto_of`
569                // therefore called `Object.create(null)` and `{}` alike.
570                h.proto_of(a) != h.proto_of(b) || h.has_null_proto(a) != h.has_null_proto(b)
571            })
572        {
573            return false;
574        }
575    }
576    let kinds = with_host(|h| {
577        let av = h.get(a).map(kind);
578        let bv = h.get(b).map(kind);
579        (av, bv)
580    });
581    seen.push((a.clone(), b.clone()));
582    let result = deep_equal_body(a, b, strict_mode, seen, kinds);
583    seen.pop();
584    result
585}
586
587fn deep_equal_body(
588    a: &Value,
589    b: &Value,
590    strict_mode: bool,
591    seen: &mut Vec<(Value, Value)>,
592    kinds: (Option<Kind>, Option<Kind>),
593) -> bool {
594    match kinds {
595        (Some(Kind::Array), Some(Kind::Array)) => {
596            let (ia, ib) = with_host(|h| (array_of(h, a), array_of(h, b)));
597            ia.len() == ib.len()
598                && ia
599                    .iter()
600                    .zip(ib.iter())
601                    .all(|(x, y)| deep_equal_seen(x, y, strict_mode, seen))
602        }
603        (Some(Kind::Object), Some(Kind::Object)) => {
604            let (ea, eb) = with_host(|h| (object_of(h, a), object_of(h, b)));
605            if ea.len() != eb.len() {
606                return false;
607            }
608            let props_match = ea.iter().all(|(k, va)| {
609                eb.iter()
610                    .find(|(k2, _)| k2 == k)
611                    .is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
612            });
613            if !props_match {
614                return false;
615            }
616            // The brands whose whole state lives in INTERNAL slots — a Date's
617            // `@@ms`, a typed array's `@@buffer`/`byteOffset`, an Error's name
618            // and message — are objects with no enumerable own properties at
619            // all. Comparing only the public ones therefore reported every pair
620            // of them as deep-equal: `deepStrictEqual(new Date(0), new Date(1))`
621            // PASSED, as did two Buffers with different bytes. Slots are
622            // compared here rather than folded into `object_of` because they are
623            // not properties — they must not affect the key COUNT above, which
624            // node takes over enumerable keys only.
625            let (ia, ib) = with_host(|h| (internals_of(h, a), internals_of(h, b)));
626            if ia.len() != ib.len() {
627                return false;
628            }
629            ia.iter().all(|(k, va)| {
630                ib.iter()
631                    .find(|(k2, _)| k2 == k)
632                    .is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
633            })
634        }
635        // A Map compares by ENTRIES and a Set by MEMBERS, both order-insensitively
636        // (`new Set([1, 2])` deep-equals `new Set([2, 1])`), so each entry on the
637        // left is matched against an as-yet-unclaimed entry on the right rather
638        // than against the one at its own index.
639        (Some(Kind::Map), Some(Kind::Map)) => {
640            let (ea, eb) = with_host(|h| (map_entries_of(h, a), map_entries_of(h, b)));
641            unordered_match(&ea, &eb, seen, |(ka, va), (kb, vb), seen| {
642                deep_equal_seen(ka, kb, strict_mode, seen)
643                    && deep_equal_seen(va, vb, strict_mode, seen)
644            })
645        }
646        (Some(Kind::Set), Some(Kind::Set)) => {
647            let (ea, eb) = with_host(|h| (set_members_of(h, a), set_members_of(h, b)));
648            unordered_match(&ea, &eb, seen, |x, y, seen| {
649                deep_equal_seen(x, y, strict_mode, seen)
650            })
651        }
652        // Two distinct RegExp objects are deep-equal when their pattern and flags
653        // are; `same_value` would call every one of them unequal.
654        (Some(Kind::RegExp), Some(Kind::RegExp)) => {
655            with_host(|h| regexp_key(h, a) == regexp_key(h, b))
656        }
657        // Everything else — strings, symbols, bigints, functions, and any two
658        // values of DIFFERENT kinds — is compared as a leaf. This arm used to be
659        // unreachable for heap values because `kind` called them all `Object`,
660        // and `object_of` then reported each as having zero properties, so any
661        // two of them matched: `deepStrictEqual('abc', 'abd')` passed silently.
662        _ => {
663            if strict_mode {
664                strict(a, b)
665            } else {
666                loose_eq(a, b)
667            }
668        }
669    }
670}
671
672/// Whether every element of `ea` can be paired off with a DISTINCT element of
673/// `eb` under `eq`. Greedy matching is enough here because the relation is an
674/// equivalence: anything that matches a claimed element would have matched
675/// whatever claimed it.
676fn unordered_match<T>(
677    ea: &[T],
678    eb: &[T],
679    seen: &mut Vec<(Value, Value)>,
680    eq: impl Fn(&T, &T, &mut Vec<(Value, Value)>) -> bool,
681) -> bool {
682    if ea.len() != eb.len() {
683        return false;
684    }
685    let mut claimed = vec![false; eb.len()];
686    'outer: for x in ea {
687        for (i, y) in eb.iter().enumerate() {
688            if !claimed[i] && eq(x, y, seen) {
689                claimed[i] = true;
690                continue 'outer;
691            }
692        }
693        return false;
694    }
695    true
696}
697
698enum Kind {
699    Array,
700    Object,
701    Map,
702    Set,
703    RegExp,
704    /// A leaf: compared with `===`, never structurally.
705    Other,
706}
707fn kind(o: &JsObj) -> Kind {
708    match o {
709        JsObj::Array(_) => Kind::Array,
710        JsObj::Object(_) => Kind::Object,
711        // A WEAK collection exposes no entries, so there is nothing to compare
712        // structurally; node treats two of them as equal only by identity.
713        JsObj::Map { weak: false, .. } => Kind::Map,
714        JsObj::Set { weak: false, .. } => Kind::Set,
715        JsObj::RegExp(_) => Kind::RegExp,
716        _ => Kind::Other,
717    }
718}
719/// A Map's entries as `(key, value)` pairs, in insertion order.
720fn map_entries_of(h: &crate::host::JsHost, v: &Value) -> Vec<(Value, Value)> {
721    match h.get(v) {
722        Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
723        _ => Vec::new(),
724    }
725}
726/// A Set's members, in insertion order.
727fn set_members_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
728    match h.get(v) {
729        Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
730        _ => Vec::new(),
731    }
732}
733/// The identity of a regular expression for comparison: its pattern and flags.
734fn regexp_key(h: &crate::host::JsHost, v: &Value) -> Option<(String, String)> {
735    match h.get(v) {
736        Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
737        _ => None,
738    }
739}
740/// An object's INTERNAL slots — the `@@`-prefixed keys that carry brand state
741/// (`@@ms`, `@@buffer`, `@@kind`) and are deliberately absent from `object_of`.
742/// Private class fields (`#`-prefixed) stay excluded: node's `deepStrictEqual`
743/// compares own ENUMERABLE properties, and a private field is neither.
744fn internals_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
745    match h.get(v) {
746        Some(JsObj::Object(p)) => p
747            .iter()
748            .filter(|(k, _)| k.starts_with("@@"))
749            .map(|(k, v)| (k.clone(), v.clone()))
750            .collect(),
751        _ => Vec::new(),
752    }
753}
754fn array_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
755    match h.get(v) {
756        Some(JsObj::Array(items)) => items.clone(),
757        _ => Vec::new(),
758    }
759}
760fn object_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
761    match h.get(v) {
762        Some(JsObj::Object(p)) => p
763            .iter()
764            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
765            .map(|(k, v)| (k.clone(), v.clone()))
766            .collect(),
767        _ => Vec::new(),
768    }
769}