Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-535 · Case file with fixtures · Case 507 of 694 · Compiler evidence

Rust Comparison Operators Require an Ordering Contract

Rust does not guess domain ordering from a struct's fields. Comparison syntax works only when the type explicitly supplies the corresponding relation.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Comparable fields do not give a containing domain type one automatic or necessarily meaningful global order.
First discriminating check
Map the operator to PartialOrd, decide whether the domain has a partial or total order, and derive only when declaration-order comparison is correct.

Two fields may each be comparable while their containing struct is not. Rust does not assume that field declaration order is also business order, and E0369 makes that missing contract visible.

The failing fixture compares two Version values with >. Both fields are u16, but Version itself has not said what “greater” means.

Operators are trait calls with domain meaning

Greater-than and less-than rely on PartialOrd. Equality relies on PartialEq. Rust operator syntax is concise, but the relationship comes from implementations rather than structural guesswork.

The official E0369 page describes the broad failure: a binary operation was attempted on a type that does not support it. The important debugging step is to map the particular token to its trait and exact operand types.

For Version, lexicographic ordering by major, then minor, is reasonable for this simplified fixture. For a network address, user record, or health report, field order might be meaningless or actively misleading.

Derive uses field declaration order

The repaired fixture derives PartialEq, Eq, PartialOrd, and Ord. Derived ordering compares corresponding fields in declaration order until it finds a difference.

That gives (2, 4) > (2, 1) and also (3, 0) > (2, 999). This matches the small version model. Changing the field order later could change ordering behaviour, so declaration order becomes observable API when deriving these traits.

I add a test containing equal major values and different minor values, plus different major values. A single happy comparison is not enough to document precedence.

Partial and total order are different promises

PartialOrd permits pairs that are not comparable. Floating-point NaN is the familiar example: partial_cmp can return None. Ord promises a total order where every pair has one consistent ordering.

Ordered maps, sets, and sorting APIs may require Ord. If my domain contains incomparable states, inventing a total order only to satisfy a container can encode false meaning. I may instead choose an explicit sort key for one view or use a hash-based collection.

The fixture's integer fields naturally form a total order, so deriving Ord is honest. This is a domain decision, not a standard response to every E0369.

Versions are more complicated in production

Real semantic versions contain pre-release identifiers where ordering follows defined rules, not naive string or tuple comparison. Build metadata may not participate in precedence. A home-grown (major, minor, patch, label) derived order could therefore be wrong while compiling perfectly.

I use a well-tested domain representation when a published specification owns the rules. If I implement comparison myself, I turn examples and edge cases from that specification into tests.

The compiler can prove that an ordering implementation exists. It cannot prove that the chosen order means what product or protocol users expect.

Explicit comparison can be clearer than implementing a global order

Sometimes I need to compare one property only:

deployed.major > required.major

Or I need a sorting policy local to one screen:

items.sort_by_key(|item| item.created_at);

Neither requirement necessarily justifies PartialOrd on the whole entity. A global trait implementation affects every generic consumer and implies one canonical relation.

I implement ordering on the type only when that relation is stable and unsurprising. Otherwise, named comparators and keys keep context visible.

Manual implementations must agree with equality

The comparison traits have laws. For a total order, values considered equal by Ord::cmp must agree with Eq; transitivity and antisymmetry matter to data structures and sorting.

A manual comparison that ignores an identifier while derived equality includes it can create inconsistent behaviour. I implement or derive the related traits together, document ignored fields, and use property tests when the relation is non-trivial.

This is one reason derive is attractive when field order exactly matches the domain: it supplies a coherent family of implementations.

My E0369 comparison checklist

  • Which operator and exact left/right types are involved?
  • Which comparison trait is missing?
  • Is there a meaningful order for whole domain values?
  • Does declaration-order derivation match that order?
  • Is the relation partial or truly total?
  • Would a local key or named comparator be clearer?
  • Do equality and ordering agree on all fields?
  • Are edge cases covered by examples, properties, or a specification?

The core principle is that ordering is knowledge about a domain, not a mechanical property of having comparable fields. I make that knowledge explicit only where it is true.