Wado

WEP: Numeric Literal Suffixes

Context

A numeric literal has no type of its own. It takes the type its context expects, and i32 or f64 where nothing expects one. Inside an expression the only way to give it another type is a cast: 255 as u8, 1.5 as f32.

A cast on a literal does two jobs at once. It types the literal as an annotation of the target would, range check included, so 300 as u8 is an error. Applied to a value, the same as converts and never checks a range, so 300_i32 as u8 would be 44. A reader has to look at the operand to know which job a cast does.

Rust gives a literal its type with a suffix, 255u8. In a hex literal the letters b and f are digits, so Rust reads 0x1f32 as the hex number 0x1F32, not as 1 of type f32.

Decision

A numeric literal may end with a type suffix: one of i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f16, bf16, f32 and f64.

let a = 255_u8;          // u8
let b = 1_000_000_i64;   // i64
let c = 1.5_f32;         // f32
let d = 0xFF_u8;         // u8
let e = 1_f64;           // f64: an integer literal with a float suffix

The _ separator is required

An underscore always separates the digits from the suffix. 255u8 is an error that says to write 255_u8. Letters written directly after a literal are read as its suffix, so 255_u9 is an error naming the suffix u9, not a number followed by a name.

The separator makes the suffix visible at a glance, where 1e5f32 and 1e5_f32 otherwise look alike. It costs one character against Rust, and the error tells a Rust writer exactly what to type.

Which literal takes which suffix

A decimal literal takes any suffix. An integer literal with a float suffix is a float, as in Rust. A float literal with an integer suffix is an error, as let x: i32 = 1.5 is.

A hex, octal or binary literal takes an integer suffix only. In a hex literal b and f are digits, so 0x1_f32 stays the hex number 0x1F32, as it is today. Reading it as a suffix would change the value of programs that already compile. In an octal or binary literal a float suffix is an error.

A suffix is a type annotation

A suffixed literal has the type its suffix names. It is checked as the same literal annotated with that type: 300_u8 and 1e40_f32 are errors, as let x: u8 = 300 and let x: f32 = 1e40 are. A minus sign in front belongs to the literal for this check, so -128_i8 is valid and -1_u8 is an error.

The context does not retype a suffixed literal. let x: i64 = 1_i32 is a type mismatch, and so is let m: Meters = 1.0_f64 for a newtype Meters. An operand beside it takes its type from it, as beside any typed value: 1_u8 + 2 adds two u8s.

A suffixed literal is a pattern of its suffix type, which must be the type of the scrutinee.

The literal_cast lint

lit as T warns where T is a suffix type and the literal can carry that suffix, and the warning gives the suffixed spelling: 255 as u8 suggests 255_u8. The cast and the suffix mean the same thing there, and the suffix says it without the reader checking the operand. 0x10 as f64 does not warn, since no suffix can write it. #[allow(literal_cast)] waives the lint for an item, and #![allow(literal_cast)] for a module.

The cast keeps its meaning on a literal. Changing it to always convert would silently change programs that compile today.

Roadmap