Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-432 · Case file with fixtures · Case 404 of 694 · Compiler evidence

A Rust Numeric Method Call May Need a Concrete Literal Type

Unsuffixed numeric literals begin with an inferred numeric type, and method lookup may require the type before fallback resolves it. Annotate the domain type at the binding or boundary, especially when width and overflow policy matter.

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
Unsuffixed literals participate in inference, while method resolution needs a concrete receiver before integer fallback can settle this otherwise unconstrained expression.
First discriminating check
Annotate the binding or literal with the domain's deliberate width and signedness, then confirm that saturation is the intended overflow policy.

I wrote let retries = 2 and then called retries.saturating_add(1). Rust reported E0689 because the integer type was ambiguous.

The failing fixture has no generic code and no unusual trait. The ambiguity comes from many integer types offering a method with the same name while no surrounding type fixes the binding.

A literal is not always i32 immediately

An unsuffixed integer literal participates in type inference. Rust can often infer its type from assignment, function arguments, arithmetic with typed values, or a return position. Integer fallback may choose i32 when inference otherwise permits it, but method resolution can require a concrete receiver type first.

The E0689 page describes a method called on an ambiguous numeric type and recommends an annotation, suffix, or cast.

Several primitive integer types implement saturating_add, so the method name does not identify one.

Put the type where the domain begins

The repaired fixture writes:

let retries: u8 = 2;

Now the method is u8::saturating_add, and the maximum is part of the model. A literal suffix such as 2_u8 would also work.

I prefer annotating the binding when several later operations depend on its type. I use a suffix for a single local constant or when it makes mixed-width arithmetic clear.

Width is a policy, not compiler decoration

Choosing u8 says the counter is nonnegative and capped at 255. Choosing usize connects it to memory indexing and target pointer width. Choosing u64 gives a stable serialized width but does not automatically make it appropriate for indexing.

The type affects overflow, casts, storage layout, protocol compatibility, and available methods. I do not accept the compiler's suggested i32 without checking the domain.

The operation also chooses an overflow policy

saturating_add clamps at the numeric maximum. Other families include checked, overflowing, wrapping, and ordinary addition with profile-dependent overflow behaviour for integers.

After solving E0689, I still ask whether saturation is correct. A retry counter might clamp safely; an account balance should probably report an error; a sequence number may intentionally wrap under a defined protocol.

The u8::saturating_add documentation gives the precise chosen operation after the receiver type is known.

Casts and annotations are not always equivalent

let retries: u8 = input requires input already to have type u8 or a valid coercion. input as u8 performs Rust's numeric cast semantics, which may truncate. u8::try_from(input) can report out-of-range values.

For a literal known to fit, annotation and suffix are simple. At an external boundary, I prefer checked conversion so solving inference does not hide data loss.

Floating-point literals have the same broad issue

Unsuffixed float literals can be f32 or f64. A method available on both may need type context. Precision, range, serialization, and hardware behaviour make the choice meaningful.

I annotate constants used in numerical algorithms and test edge cases such as infinities, NaN, signed zero, and rounding when they matter. The Atlas has separate cases for those runtime semantics.

Generic numeric code needs trait design

If the intent is truly to support several numeric types, adding one concrete annotation defeats that goal. I give the function a generic parameter and the exact traits it needs, or use a domain trait with explicit overflow semantics.

Primitive inherent methods do not automatically form one generic interface. A crate may supply numeric traits, but its version and bounds become part of the API.

Tests should pin the boundary, not inference accidents

I assert behaviour at zero, one below the maximum, the maximum, and any conversion boundary. I do not write a test whose only success depends on an unsuffixed literal becoming today's fallback type. Explicit fixture types make failures explain the arithmetic policy instead of a compiler inference detail, and they remain readable when a surrounding expression changes.

My inference checklist

  • Which literal or binding lacks a concrete type?
  • What surrounding context was expected to constrain it?
  • Which widths provide the called method?
  • Is signedness meaningful?
  • What overflow policy does the method encode?
  • Is the value serialized, indexed, or passed across FFI?
  • Does conversion need range checking?

The core principle is that numeric syntax leaves room for inference, while numeric behaviour belongs to a concrete type. I annotate at the domain boundary so method lookup, overflow policy, and representation all follow one deliberate decision.