Numeric Type Inference
When you write an unsuffixed numeric literal in Rust—such as 42 or 3.14—it does not immediately lock into a single concrete type. Instead, rustc creates a temporary type variable ({integer} for integer literals, {float} for floating-point literals) and resolves it to a concrete numeric type by inspecting how the variable is used later in the function, falling back to i32 or f64 only if no external constraint appears.
How {integer} and {float} Unification Works
let x = 42; <-- Step 1: `x` is assigned temporary type `{integer}`
│
├── Case A: `x` is later passed to `Vec::with_capacity(x)` (which expects `usize`)
│ ==> `{integer}` unifies with `usize`! (`x` has type `usize` from line 1)
│
└── Case B: `x` is only printed or used in unconstrained arithmetic (`x + 1`)
==> `{integer}` falls back to the default integer type `i32`!
| Literal Kind | Compiler Placeholder | Default Fallback Type (If Unconstrained) | Possible Concrete Types It Can Infer To |
|---|---|---|---|
Unsuffixed integer (42, 0xFF, 0b1010) |
{integer} |
i32 |
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize |
Unsuffixed float (3.14, 1.0e-5) |
{float} |
f64 |
f32, f64 |
Suffixed literal (42_u64, 3.14_f32) |
Concrete immediately | N/A | Locked to u64 or f32 at the literal site |
Contextual Clues That Drive Numeric Inference
The compiler's Hindley-Milner type inference engine gathers constraints across the entire function body—even from statements many lines below the let declaration:
fn require_u16(port: u16) {
println!("Port (u16): {port}");
}
fn main() {
// 1. Inferred from a later function call:
let port = 8080; // `{integer}` -> resolved to `u16` because of `require_u16(port)` below!
require_u16(port);
// 2. Inferred from slice/array indexing (which always requires `usize`):
let data = ["alpha", "beta", "gamma"];
let idx = 1; // `{integer}` -> resolved to `usize` because of `data[idx]`!
println!("Element at idx: {}", data[idx]);
// 3. Inferred from binary arithmetic with a known type:
let explicit_u64: u64 = 1000;
let delta = 250; // `{integer}` -> resolved to `u64` because of `explicit_u64 + delta`!
println!("Sum (u64): {}", explicit_u64 + delta);
// 4. Unconstrained literal falls back to `i32` (for integer) and `f64` (for float):
let default_int = 10;
let default_float = 2.5;
println!("Defaults: {default_int} (i32), {default_float} (f64)");
}
Port (u16): 8080
Element at idx: beta
Sum (u64): 1250
Defaults: 10 (i32), 2.5 (f64)
Container Element Inference (Vec::new() + .push())
When you create an empty collection like let mut list = Vec::new(); and later push an integer or float literal into it, both the collection's element type and the literal's numeric type unify simultaneously:
fn main() {
let mut bytes = Vec::new(); // Type starts as `Vec<?T>`
bytes.push(255_u8); // `?T` unifies with `u8` -> `bytes` is `Vec<u8>`
bytes.push(42); // Unsuffixed `42` (`{integer}`) is inferred as `u8`!
println!("bytes: {bytes:?}");
}
bytes: [255, 42]
Conflicting Numeric Constraints (E0308)
A single variable can only ever have one concrete type. Once an {integer} variable x is unified with u16 (for example, by passing it to a function expecting u16), attempting to also use x where a u32 or usize is expected causes a compile-time type mismatch:
// ❌ does not compile
fn takes_u16(n: u16) {
println!("{n}");
}
fn takes_u32(n: u32) {
println!("{n}");
}
fn main() {
let x = 100;
takes_u16(x); // Locks `x` to type `u16`!
takes_u32(x); // Error: expected `u32`, found `u16`!
}
error[E0308]: mismatched types
--> src/main.rs:12:15
|
11 | takes_u16(x); // Locks `x` to type `u16`!
| --------- - here the type of `x` is inferred to be `u16`
12 | takes_u32(x); // Error: expected `u32`, found `u16`!
| --------- ^ expected `u32`, found `u16`
| |
| arguments to this function are incorrect
|
help: you can convert a `u16` to a `u32`
|
12 | takes_u32(x.into());
| +++++++
error: aborting due to 1 previous error
Out-of-Range Literals (overflowing_literals)
When an {integer} literal is unified with a specific integer type (or defaults to i32), the compiler checks that the literal value fits inside that type's [MIN..=MAX] range:
- If you write
let big = 5_000_000_000;with no type constraint,{integer}defaults toi32(MAX = 2_147_483_647) and triggers anoverflowing_literalserror advising you to add: i64or: u64. - If you write
let byte: u8 = 300;,{integer}unifies withu8(0..=255) and fails to compile:
// ❌ does not compile
fn main() {
let byte: u8 = 300;
}
error: literal out of range for `u8`
--> src/main.rs:2:20
|
2 | let byte: u8 = 300;
| ^^^
|
= note: the literal `300` does not fit into the type `u8` whose range is `0..=255`
= note: `#[deny(overflowing_literals)]` on by default
error: aborting due to 1 previous error
Ambiguous Numeric Method Calls (E0689)
Calling an inherent method (such as .abs(), .pow(2), or .sqrt()) directly on an unsuffixed numeric literal or unconstrained {integer} / {float} variable before its concrete width is known fails with E0689 because the compiler refuses to guess whether you want f32::sqrt or f64::sqrt:
// ❌ does not compile
fn main() {
let x = 16.0;
println!("{}", x.sqrt());
}
error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`
--> src/main.rs:3:22
|
2 | let x = 16.0;
| - you must specify a type for this binding, like `f64`
3 | println!("{}", x.sqrt());
| ^^^^
|
help: you must specify a concrete type for this numeric value, like `f64`
|
2 | let x: f64 = 16.0;
| +++++
error: aborting due to 1 previous error
Specify either a variable type annotation (let x: f64 = 16.0;) or a literal suffix (16.0_f64.sqrt()) whenever calling inherent numeric methods.