Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-575 · Case file with fixtures · Case 547 of 694 · Compiler evidence

Rust Unary Operators Require Their Own Trait Contract

Operator meaning is attached to explicit traits, not inferred from a field's representation. Implement a lawful domain operation or expose a named method.

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
The inner field's representation was assumed to grant behaviour to a distinct domain type whose operator contract has not been declared.
First discriminating check
Decide whether negation has one lawful meaning, then implement Not with tests or retain a named method when the operation is contextual or fallible.

Wrapping a bool in a domain type stops unrelated boolean operations from happening accidentally. It also means the wrapper does not inherit the ! operator. When the failing fixture writes !enabled, rustc reports E0600 because Enabled has no Not implementation.

Representation does not define operator meaning

The one field inside Enabled is a boolean, but the outer type is a new semantic contract. Rust does not inspect fields and guess which operations should be forwarded. This is important because many wrappers intentionally restrict or change what their representation can do.

A UserId(u64) should not automatically support arithmetic merely because u64 does. A Meters(f64) may support addition with meters but not addition with seconds. A permission state may need a named transition rather than logical negation.

The E0600 explanation points to the relevant operator trait. For !, that trait is std::ops::Not. The trait chooses an Output type, so negation does not even have to return the same type as its input.

The repair makes the algebra explicit

The repaired fixture implements Not for Enabled and returns another Enabled whose inner boolean is reversed. Its assertion captures the expected law for one value.

For a production type, I add the complementary case and useful algebraic properties. Double negation should return the original state if this really models boolean negation. If the type has more than two states, such as Enabled, Disabled, and Inherited, a unary ! may be a poor abstraction because the result for Inherited is not obvious.

In that case I prefer a named method such as toggle_explicit, deny, or resolve_against(default). Named operations have more room to communicate fallibility and context.

Unary operators have separate contracts

Implementing one operator does not imply another. Unary - uses Neg; ! uses Not; dereference syntax involves Deref and DerefMut. The operator-expression reference documents which syntax maps to which behaviour.

This helps with generic code. A bound such as T: Not<Output = T> says precisely what the algorithm needs. It does not assume T is a primitive boolean or integer. Conversely, if an algorithm only needs a predicate, accepting a closure or a method returning bool can be more general and easier to understand.

I pay attention to ownership too. Not::not(self) consumes its receiver. For a small Copy wrapper this may be invisible; for a larger non-Copy domain value it is a real move. Implementations for references are possible when borrowing is the intended operation, but adding many combinations can make an API noisy. A named method taking &self may better express a computed view.

Operator implementations are public API

Once users write compact syntax, its meaning becomes difficult to change. I document edge cases and avoid surprising side effects. Operators should usually be cheap enough and unsurprising enough that an expression remains readable.

For error-prone operations I prefer methods returning Result or Option. The operator traits generally return their Output directly, and forcing hidden panics or sentinel values into that contract weakens the type.

I also check trait coherence. Either the trait or the type must be local for an implementation to be permitted. A local newtype is a clean way to define domain operators around foreign representations without attempting to change global behaviour for primitive types.

My E0600 checklist

  • Which unary syntax failed, and which standard operator trait controls it?
  • Is the receiver a primitive, reference, smart pointer, or domain wrapper?
  • Does the operation have one unsurprising meaning for every valid value?
  • What should the associated Output type be?
  • Is consuming self appropriate for this value?
  • Which laws, boundary values, and involutions should tests cover?
  • Would a named, fallible, or contextual method communicate more honestly?
  • Is a local newtype needed to satisfy coherence and protect domain semantics?

The core principle is that syntax does not leak automatically through representation. Rust makes me grant operator behaviour deliberately. I use E0600 to decide whether the domain has a lawful compact operation, and I keep the type restrictive when it does not.