RFA-602 · Case file with fixtures · Case 574 of 694 · Compiler evidence
Rust Numeric Method Calls Need a Concrete Number Type
A numeric literal can fit several primitive types. Constrain the type from domain precision, storage, or API requirements before invoking type-specific methods.
- 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
- Literal syntax admits multiple numeric types, while method resolution needs a concrete receiver before surrounding context selects precision.
- First discriminating check
- Choose width and precision from the domain or external API and state it with a binding annotation or literal suffix.
The literal 2.0 could become f32 or f64 depending on context. When code immediately calls a type-specific inherent method and no other constraint selects the type, rustc cannot know which method set to use. The failing fixture calls sqrt and gets E0689.
Literal fallback does not solve every method lookup
Rust has default numeric types in unconstrained situations, but method resolution sometimes needs a concrete receiver before fallback supplies enough information. Both floating primitives expose related methods, and rustc asks the program to choose.
The official E0689 explanation shows annotations, suffixes, and casts as ways to constrain a numeric literal or binding. I prefer an annotation or suffix that reflects the domain rather than a cast added only at the failing call.
The issue also appears with integers when a method exists on several possible integer types and the surrounding expression provides no width or signedness.
The repair selects f64 deliberately
The repaired fixture writes let value: f64 = 4.0 and asserts that its square root is two. The type is now stable throughout the computation.
Why f64? For a general application calculation it is a common precision choice, but that is not universal. Graphics, machine learning, embedded hardware, network formats, and SIMD workloads may intentionally use f32. External APIs and storage schemas may mandate one.
I select from accuracy needs, performance measurements, interoperability, and memory volume. E0689 usefully forces that policy to appear somewhere.
Suffix, binding annotation, and API context differ in reach
Writing 4.0_f64 constrains that literal. Annotating value: f64 documents the binding and allows later literal changes. Passing the value to a function accepting f64 can provide context without local annotation.
An as f64 cast is more appropriate when converting an already typed value. For an untyped literal, a suffix is clearer and avoids suggesting a runtime conversion.
The Reference documents floating-point literals, including suffix syntax. I avoid unnecessary long decimal literals that imply accuracy the binary representation cannot provide.
Floating operations need semantic tests
The f64::sqrt operation has defined behaviour for negative numbers, infinities, NaN, and signed zero. Exact equality is suitable for the fixture's perfect square, not for every numerical algorithm.
Production tests use domain tolerances and consider absolute versus relative error. I test boundary magnitudes, NaN policy, overflow, underflow, and platform requirements where relevant. Changing f64 to f32 can alter convergence and accumulated error without compiler diagnostics.
For money and exact decimal quantities, binary floating point may be the wrong type entirely. A fixed-point integer or reviewed decimal library can express required rounding. Resolving E0689 by picking any float would miss the larger invariant.
Generic numeric APIs should expose their requirements
Rust has no single built-in “number” trait covering every operation with one universal algebra. Generic algorithms state the operator and method bounds they need, often through focused traits or domain abstractions.
If a function fundamentally requires square root, its type contract should say so through a chosen primitive or appropriate trait. Letting ambiguous literals reach deep into the body makes diagnostics harder.
I also watch unsuffixed constants in macros. Expansion context can differ across call sites. Requiring a target type or using explicitly typed constants makes generated code predictable.
Named constants are useful when one precision decision should remain consistent across several formulas and tests.
My E0689 checklist
- Which numeric literal or binding lacks a concrete primitive type?
- Which receiver types provide the requested method?
- Does domain precision or external representation require f32, f64, or another type?
- Is a literal suffix or binding annotation clearer than a cast?
- Could fixed-point or decimal representation better preserve the invariant?
- What are the method's NaN, infinity, overflow, and rounding behaviours?
- Do tests use an appropriate comparison policy for numerical results?
- Are macros and generic APIs giving literals enough stable context?
The core principle is that numeric syntax does not decide numeric semantics. Width, signedness, and precision affect methods, representation, and results. I treat E0689 as the point to make that choice explicit from evidence rather than accept an accidental default.