RFA-534 · Case file with fixtures · Case 506 of 694 · Compiler evidence
Implementing Add Does Not Automatically Provide AddAssign in Rust
Producing a new sum and updating an existing place are separate operator contracts. Rust does not infer assignment behaviour from Add.
- 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
- Producing a new output and updating an existing mutable place admit different types and semantics, so Rust does not derive one operator trait from the other.
- First discriminating check
- Map the compound operator to its exact Assign trait, verify the left side is writable, and decide whether in-place mutation is a truthful domain contract.
I used to expect += to be a convenient rewrite of left = left + right. For builtin numbers it often feels that way. With generic or domain types, Rust deliberately treats the two operations as different contracts.
The failing fixture implements Add for Credits, so Credits(4) + Credits(3) is valid. The assignment form still emits E0368 because no AddAssign implementation exists.
The operator spelling selects a trait
+ uses std::ops::Add. It consumes or borrows operands according to the implementation and produces an associated Output type.
+= uses std::ops::AddAssign. Its central method receives &mut self and updates that existing value in place. It has no associated output because the destination is the left-hand place itself.
The official E0368 explanation explicitly notes that implementing Add does not automatically implement AddAssign.
Rust cannot safely invent the assignment contract
An Add implementation may return a type different from Self. Matrix plus vector, timestamp plus duration, and borrowed views plus owned buffers can all have useful custom outputs. Such an operation cannot always be written back into the original left-hand type.
Even when Output = Self, assignment may have different performance, overflow, validation, or allocation behaviour. Rust requires the author to make that promise explicitly instead of deriving semantics from coincidence.
This separation is valuable in generic APIs. A bound of T: AddAssign says the algorithm can accumulate into an existing T; T: Add<Output = T> says it can create a new T. Neither capability silently stands in for the other.
The repaired type implements the operation it exposes
The repaired fixture adds an AddAssign implementation and updates the inner integer through self.0 += other.0. The caller's total += Credits(3) now has a direct trait implementation.
For a small newtype this code is straightforward. For a larger structure, I consider exception-like partial states even though Rust has no language exceptions. If validation can fail halfway through mutating several fields, an infallible AddAssign may be the wrong interface.
I may instead provide a checked method returning Result, compute a validated replacement first, or expose checked_add. Familiar punctuation should not hide a fallible domain transition.
Rewriting the call is another valid repair
If the type intentionally provides only Add, callers can write:
total = total + delta;
This consumes the old total and stores the new output. It is not equivalent for every type, but when Add<Output = Self> is the intended contract it can avoid widening the public API.
I choose based on how the type is used. Repeated accumulation benefits from AddAssign; occasional value composition may need only Add. Implementing every possible operator merely for symmetry can create contracts that later become difficult to change.
Generic bounds reveal the difference quickly
An accumulator written as total += item needs something like T: AddAssign<Item>. Changing its body to total = total + item requires T: Add<Item, Output = T> and moves the previous value.
These signatures admit different types. The assignment version can work with a type that updates a buffer efficiently but does not define an owned sum. The producing version can work with outputs or ownership patterns that do not support mutation.
I write the algorithm first in domain language—“update this accumulator” or “derive a new combined value”—then select the corresponding trait.
Assignment also requires a writable place
Even with AddAssign, the left side must be a mutable place expression. A literal, immutable binding, or temporary has nowhere appropriate to retain the mutation. That produces other diagnostics and requires a different repair.
My debugging order is therefore:
- Is the left side a writable place?
- Is its binding or path mutably accessible?
- Does its type implement the exact assignment trait for the RHS type?
- Are borrowed variants involved?
This avoids adding mut when the actual missing piece is AddAssign, or implementing a trait when the left side is not storage.
My E0368 checklist
- Which compound assignment operator did I write?
- Which
std::ops::*Assigntrait does it select? - Does the implementation exist for this exact right-hand type?
- Is the left side a mutable place expression?
- Would producing and replacing a value preserve the intended semantics?
- Can the operation fail, overflow, or violate a domain invariant?
- Does repeated accumulation need an efficient in-place path?
- Should the public type support this operator at all?
The core principle is that similar punctuation does not imply the same capability. I implement AddAssign when in-place accumulation is a real promise, and I keep Add separate as the contract for producing a result.