ADR-204: Whether a Type Compares Is a Question About Its Parts, and a Type That Does Not Is Refused Here

Status: Accepted Date: September 23, 2026 Related: ADR-203 D2 (the unit-only case, which this subsumes), ADR-005 §1 Group B and ADR-123 D1 (the structural walk and the hand-written column this is modelled on), ADR-010 D1 (absence is not permission), ADR-288 (the emitter has no types, so the checker answers)

1. Context

Without a decision, a == b over a declared type has no lowering:

error: binary operation `==` cannot be applied to type `P`
     = an implementation of `PartialEq` might be missing for `P`
     = consider annotating `P` with `#[derive(PartialEq)]`

on the author’s line, in the backend’s words about a file nobody wrote, with a help the source cannot take — Part III C.1 and C.2 at once.

A blanket derive is the wrong answer: it would refuse the declaration of every type holding a Locked[T], a Mapped or a socket, which is a worse place to be wrong than the comparison (#157).

2. Decisions

D1 — A type compares when every part of it does, walked structurally.

The walk is the one ADR-005 §1 Group B already asks for may this cross a thread, pointed at a different question:

Absence is not permission (ADR-010 D1). A std type without the line does not compare: a lock, a mapping, a socket, a task’s handle, a channel’s ends. compares = true is a claim reviewed like code, which is exactly what crosses = true is and for the same reason — nothing in the compiler can walk a Rust type’s parts.

A type this compiler could not work out compares, which is the answer an absent claim gets everywhere (Part III C.4): a refusal on a guess is a correct program refused, and it is the worse of the two mistakes.

Where one rule ends and the other begins is whether anything knows the name (#379):

nikaia describe reads an impl PartialEq for T as it reads the derive, so the line a hand-written comparison earns is drafted rather than left for the reviewer to notice. Only a trait written without an argument counts - PartialEq<str> compares the type with something else - and only for a type in the same module as the impl; one elsewhere is the reviewer’s line to add.

D2 — Eq is the same walk with one primitive answering differently.

Rust’s f64 is PartialEq and not Eq, because NaN != NaN. So a float compares and is not an equivalence, and every type holding one differs with it, however far down. It is the same recursion with one arm changed rather than a look at the type’s syntax: Json::Number(f64) makes Json PartialEq and not Eq, so a struct holding a Json is not Eq either.

A library type is never taken for an equivalence. Whether one is, is a second claim nobody has had a use for, so a struct holding a time::Duration compares and is not a map key yet. That is the under-approximation that cannot be wrong, and the day something needs it the column gains a second value rather than a guess.

D3 — == on a type that does not compare is NK1188, here.

Changed by ADR-341 D5: a program may write impl Eq for a type it declares, which replaces the equality the walk would have derived.

The refusal names the type and says what decides it, and its help is one a program can take: compare the parts that carry the answer, or give the type a method that says what equality means for it. There is no #[derive] to write, which is what makes the backend’s help a way out that cannot be taken.

Asked of one side and only where its type is known. The two sides disagreeing is NK1102’s business one refusal over, and a type this compiler could not work out is left alone.

3. Consequences

4. What this does not decide

This site is open source. Improve this page.