Feature Name: numeric-conversions

Start Date: 2019-10-30

RFC PR: rust-lang/rfcs#0000

Rust Issue: rust-lang/rust#0000

Summary

Add explicitly-named standard library APIs for conversion between primitive number types with various semantics: truncating, saturating, rounding, etc.

This RFC does not attempt to define general-purpose traits that are intended to be implemented by non-primitive types, or to support code that wants to be generic over number types.

Motivation

Status quo as of Rust 1.39

The as keyword allows converting between any two of Rust’s primitive number types:

u8

u16

u32

u64

u128

i8

i16

i32

i64

i128

usize

isize

f32

f64

However the semantics of that conversion varies based on the combination of input and output type. The Rustonomicon documents:

casting between two integers of the same size (e.g. i32 -> u32) is a no-op

casting from a larger integer to a smaller integer (e.g. u32 -> u8) will truncate

casting from a smaller integer to a larger integer (e.g. u8 -> u32) will zero-extend if the source is unsigned sign-extend if the source is signed

casting from a float to an integer will round the float towards zero NOTE: currently this will cause Undefined Behavior if the rounded value cannot be represented by the target integer type . This includes Inf and NaN. This is a bug and will be fixed.

casting from an integer to float will produce the floating point representation of the integer, rounded if necessary (rounding to nearest, ties to even)

casting from an f32 to an f64 is perfect and lossless

casting from an f64 to an f32 will produce the closest possible value (rounding to nearest, ties to even)

(Note: the proposed fix for the float to integer case is to make the conversion saturating.)

Additionally, the general-purpose From trait (and therefore TryFrom through the blanket impl<T, U> TryFrom<U> for T where U: Into<T> ) is implemented in cases where the conversion is exact: when every value of the input type is converted to a distinct value of the output type that represents exactly the same real number.

The TryFrom trait is also implemented for the remaining combinations of integer types, returning an error when the input value is outside of the MIN..=MAX range supported by the output type. For this purpose usize and isize are conservatively considered to be potentially any size of at least 16 bits, to avoid having non-portable From impls that only exist on some platforms.

The table below exhaustively lists those impls, with F indicating a From impl and TF indicating (only) TryFrom . Rows are input types, columns outputs.

↬ u8 u16 u32 u64 u128 i8 i16 i32 i64 i128 usize isize f32 f64 u8 F F F F F TF F F F F F F F F u16 TF F F F F TF TF F F F F TF F F u32 TF TF F F F TF TF TF F F TF TF F u64 TF TF TF F F TF TF TF TF F TF TF u128 TF TF TF TF F TF TF TF TF TF TF TF i8 TF TF TF TF TF F F F F F TF F F F i16 TF TF TF TF TF TF F F F F TF F F F i32 TF TF TF TF TF TF TF F F F TF TF F i64 TF TF TF TF TF TF TF TF F F TF TF i128 TF TF TF TF TF TF TF TF TF F TF TF usize TF TF TF TF TF TF TF TF TF TF F TF isize TF TF TF TF TF TF TF TF TF TF TF F f32 F F f64 F

Preferring explicit semantics

When looking at code with a $expr as $ty cast expression, the semantics of the conversion are often not obvious to human readers. Deducing the type of the input expression usually requires looking at other parts of the code, possibly distant ones. In some cases it’s even possible to make the compiler infer the output type, with syntax like foo() as _ .

It’s also possible for those types to change when a possibly-distant part of the code is modified. A cast that was previously exact could suddenly have truncation semantics, which might be incorrect for a given algorithm.

To avoid this, it’s preferable to use for example an explicit u32::from(foo) call instead of casting with as . In fact Clippy has a lint for exactly this (though a silent by default one).

In some other cases however, truncation or some other conversion semantics might be the desired behavior. Communicating that intent to human readers is just as useful then as it would be with a from call.

(Not yet) deprecating the as keyword

Because of the ambiguity described above, deprecating as casts entirely has been discussed before.

Providing an alternative with something like this RFC would be a prerequisite, but this RFC is not proposing such a deprecation.

Guide-level explanation

For the purpose of conversion semantics, Rust has two kinds of primitive number types: floating point and integer. This makes four combinations of input and output kind.

For a given conversion let’s call:

I the input type

the input type i the input value

the input value O the output type

the output type o the output value, the result of the conversion: let o: O = convert(i: I);

Exact conversions

For combinations of primitive number types where they are implemented, the general-purpose convert::Into and convert::From traits offer exact conversion: o always represents the exact same real number as i .

The I::into(self) -> O method and I::from(O) -> Self constructor are available without importing the corresponding trait explicitly, since the traits are in the prelude.

Integer to integer

For all combinations of primitive integer types I and O , the standard library additionally provides:

The I::try_into<O>(self) -> Result<O, E> method and O::try_from<I>(I) -> Result<Self, E> constructor for fallible conversion . These are inherent methods of primitive integers that delegate to the general-purpose convert::Into and convert::From traits. Although these traits are not in the prelude, they do not need to be in scope for the inherent methods to be called. This returns an error when i is outside of the range that O can represent. The error type E is either convert::Infallible (where a From is also implemented) or num::TryFromIntError .

The I::modulo_to<O>(self) -> O I::wrapping_to<O>(self) -> O method for wrapping conversion , also known as bit-truncating conversion . In terms of arithmetic, o is the only value that O can represent such that o = i + k×2ⁿ where k is an integer and n is the number of bits of O . In terms of memory representation, this takes the n lower bits of the input value. The upper bits are truncated off. This is an a sense opposite of float-to-integer truncation where the less-significant fractional part is truncated off. For example, 0xCAFE_u16 maps to 0xFE_u8 , and 130_u32 to -126_i8 . Note: This is the behavior of the as operator.

The I::saturating_to<O>(self) -> O method for saturating conversion. o is the value arithmetically closest to i that O can represent. This is O::MIN or O::MAX for underflow or overflow respectively.

Float to float, integer to float

For all combinations of primitive number types I (floating point or integer) and primitive floating point type O , the standard library additionally provides:

I::round_to<O>(self) -> O I::approx_to<O>(self) -> O for approximate conversion. o is the value arithmetically closest to i that O can represent. Overflow produces infinity of the same sign as i . For floating point I , rounding may happen due to precision loss through fewer mantissa bits. For integer I , rounding may happen for large values (positive or negative). Rounding is according to roundTiesToEven mode as defined in IEEE 754-2008 §4.3.1: pick the nearest floating point number, preferring the one with an even least significant digit if exactly halfway between two floating point numbers. Note: This is the behavior of the as operator.

Float to integer

For all combinations of primitive floating point type I and primitive integer type O , the standard library additionally provides:

I::saturating_to<O>(self) -> O for saturating truncating conversion . The fractional part of i is truncated off in order to keep the integral part. That is, the value is rounded towards zero. Underflow maps to O::MIN . Overflow maps to O::MAX . NaN maps zero. Note: this may become the behavior of the as operator in a future Rust version.

I::unchecked_to<O>(self) -> O for unsafe truncating conversion. The fractional part of i is truncated off in order to keep the integral part. That is, the value is rounded towards zero. This method is an unsafe fn . It has Undefined Behavior if i is infinite, is NaN , or cannot be represented exactly in O after truncation. Note: This is the behavior of the as operator as of Rust 1.39, even though it can be used outside of any unsafe block or function.

Reference-level explanation

Everything discussed in this RFC is defined in the core crate and reexported in the std crate.

Exact, fallible, and unsafe truncating conversion conversions described above already exist in the standard library. FIXME: this assumes PR #66852 and PR #66841 are accepted and have landed.

Inherent methods are added that delegate calls to the corresponding trait method. They are generic to support multiple return types. Some of these impl s are macro-generated, to reduce source code duplication:

impl $Int { // Added in https://github.com/rust-lang/rust/pull/66852 pub fn try_from<T>(value: T) -> Result<Self, Self::Error> where Self: TryFrom<T> { /* … */} pub fn try_into<T>(self) -> Result<T, Self::Error> where Self: TryInto<T> { /* … */} pub fn wrapping_to<T>(self) -> T where Self: IntToInt<T> { /* … */} pub fn saturating_to<T>(self) -> T where Self: IntToInt<T> { /* … */} pub fn approx_to<T>(self) -> T where Self: IntToFloat<T> { /* … */} } impl $Float { pub fn approx_to<T>(self) -> T where Self: FloatToFloat<T> { /* … */} pub fn saturating_to<T>(self) -> T where Self: FloatToInt<T> { /* … */} // Added in https://github.com/rust-lang/rust/pull/66841 pub unsafe fn unchecked_to<T>(self) -> T where Self: FloatToInt<T> { /* … */} }

Four supporting traits are added to the convert module:

mod private { pub trait Sealed {} } pub trait IntToInt<T>: self::private::Sealed { // Supporting methods… } pub trait IntToFloat<T>: self::private::Sealed { // Supporting methods… } pub trait FloatToFloat<T>: self::private::Sealed { // Supporting methods… } pub trait FloatToInt<T>: self::private::Sealed { // Supporting methods… }

Each trait has methods with the same signatures as inherent methods that delegate calls to them.

The sealed trait pattern is used to prevent impl s outside of the standard library. This will allow adding more methods after the traits are stabilized. See Future possibilities below.

The traits are implemented for all relevant combinations of types. Again, some of these impl s are macro-generated:

impl IntToInt<$OutputInt> for $InputInt { /* … */ } impl IntToFloat<$OutputFloat> for $InputInt { /* … */ } impl FloatToFloat<$OutputFloat> for $InputFloat { /* … */ } impl FloatToInt<$OutputInt> for $InputFloat { /* … */ }

Drawbacks

This adds a significant number of items to libcore. However primitive number types already have numerous inherent methods and trait methods, so this isn’t unprecedented.

If the as keyword is never deprecated or until it is, we would in many cases have two ways of doing the same thing.

Rationale and alternatives

The “shape” of the API could be different. Namely, instead of inherent methods that delegate to supporting traits we could have:

Plain trait methods, with traits that need to be imported into scope. This less convenient to users.

Plain trait methods, with traits in the prelude. The bar is generally high to add anything to the prelude.

Non-generic inherent methods that include the name name of the return type in their name: wrapping_to_u8 , wrapping_to_i8 , wrapping_to_u16 , … This causes multiplicative explosion of the number of new items.

This RFC however makes no active attempt at supporting callers who are themselves generic to support multiple number types. Traits are only used as a way to avoid multiplicative explosion.

This RFC proposes adding multiple conversions methods with various semantics even for combinations of types where they are “useless” because the conversion is always exact. For example, u8::wrapping_to<i32> and u8::saturating_to<i32> both behave the same as <u8 as Into<i32>>::into . This avoids the question of what to do about the portability of impls for usize and isize .

In the case of float to float conversion specifically, I = f64 and O = f32 is the only combination that is really useful. We could have only f64::approx_to(self) -> f32 instead of generic methods with a trait. Keeping a trait anyway makes this more consistent with the other kinds of conversions, and is compatible with a future addition of new primitives floating point types ( f16 , f80 , …) in case those are ever desired.

Prior art

FIXME

Unresolved questions

FIXME

Future possibilities

This pattern of API is extensible and supports adding more methods with different conversion semantics. For example:

Wrapping approximate floating point to integer conversion that “wraps around” instead of saturating. (But what to do about infinities and NaN ?)

Fallible approximate floating point to floating point conversion that returns an error instead of mapping a finite value to infinity

Fallible approximate floating point to integer conversion that returns an error for NaN and instead of saturating to MAX or MIN .

Fallible exact conversion that never rounds and returns an error if the input value doesn’t have an exact representation in the output type, for some subset or all of: Integer to floating point Floating point to integer Floating point to floating point



This RFC doesn’t explore which of these (or others) are useful enough to merit adding to the standard library.