pre-RFC: primitive abstraction traits

The purpose of these traits is exclusively scoped to abstracting over the sets of primitive types which already exist in the language for numeric literal type inference β€” namely, {integer} and {float}. Attempting to generalize further to include non-primitive arithmetic types is an explicit non-goal left to ecosystem crates like num-traits. Further refinement and/or extension of the trait hierarchy (e.g. a trait covering all numeric primitives, or traits for just signed or unsigned integers) are deferred for the time being and left to ecosystem crates such as funty to provide.

Why? Rust code has gotten along perfectly fine without such traits so far, either by code just using a large enough numeric type instead of being generic, or by using a macro to stamp out a trait implementation across multiple primitive types. A primary reason is pedagogy and optics; it's a very easy desire to have, seeing the multiple numeric types, to write code that works for any of them. Having to fall back to the hammer that is syntactic macros to accomplish what seems on the surface like a relatively simple ask doesn't feel great. And people do want to write such code in practice; a look at the reverse dependencies of the aforementioned crates gives a partial picture of the crates doing so with generics and missing crates doing so with internally defined macros.

To the core::primitive module, we add two traits.

(Edit context: Removed a shared Primitive supertrait which was sealed and implemented for the types reexported in the core::primitive module. Discussion here reached consensus that it wasn't adding anything worthwhile.)

Sealing is done as a matter of future extensibility. By sealing the primitive abstraction traits, we can add new functionality onto the traits in a nonbreaking manner to mirror new functionality added to the primitive types themselves.

All of the core::primitive traits are #[fundamental], meaning that downstream implementations can rely on types other than the primitive types not implementing the primitive abstraction traits. Notably, this would hopefully allow downstream blanket impls over T: Integer and T: Float to not conflict.

#[sealed]
#[fundamental]
pub trait Integer: 'static
    + Sized + Copy + Clone
    + Send + Sync + Unpin
    + UnwindSafe + RefUnwindSafe
    + // every trait impl macro-pasted for all integers
{
    // every associated item macro-pasted for all integers
}

impl Integer for i8 { /* ... */ }
impl Integer for i16 { /* ... */ }
impl Integer for i32 { /* ... */ }
impl Integer for i64 { /* ... */ }
impl Integer for i128 { /* ... */ }
impl Integer for isize { /* ... */ }

impl Integer for u8 { /* ... */ }
impl Integer for u16 { /* ... */ }
impl Integer for u32 { /* ... */ }
impl Integer for u64 { /* ... */ }
impl Integer for u128 { /* ... */ }
impl Integer for usize { /* ... */ }

#[sealed]
#[fundamental]
pub trait Float: 'static
    + Sized + Copy + Clone
    + Send + Sync + Unpin
    + UnwindSafe + RefUnwindSafe
    + // every trait impl macro-pasted for all floats
{
    // every associated item macro-pasted for all floats
}

impl Float for f32 { /* ... */ }
impl Float for f64 { /* ... */ }

To reiterate, the set of functionality provided by these traits is intended to be dynamic and grow as more functionality is added to primitive integers/floats. The one small wrinkle is that the functionality provided should not prevent the addition of new primitive arithmetic types (e.g. f16, f128, u256) in the future, should those be desired.

An additional possibility is to replace the current definition of unsafe fn f32::to_int_unchecked<Int>(self) -> Int where Self: FloatToInt<Int> with a more usual looking unsafe fn f32::to_int_unchecked<Int: Integer>(self) -> Int, though FloatToInt would likely remain as an an implementation detail. Similarly, other rustdoc signatures (and implementors lists) could be simplified to use these traits instead of listing every component type separately.

If we want to be extra careful and reserve the right to move some items onto a supertrait instead (e.g. a shared primitive::Numeric trait[1] or some ops::WrappingOp trait families), it should be possible to put the items on an unnameable supertrait, such that they can be used with method syntax or Type::item, but not Trait::item. However, the items suggested to live on this trait do so because they are inherent items already, and must stay so for stability reasons; thus the RFC author believes that putting them on the trait should not add any additional stability concerns. The only functionality shared between {integer} and {float} is as interconversion (which isn't available by method yet) and interconversion to byte arrays (which isn't provided on the trait yet as doing so is dependent on incomplete nightly features). Supertraits can be moved onto a shared supertrait backwards compatibly.

Additionally, the fact that these traits are (deliberately) not object safe improves their forward compatibility to extension, e.g. by not having to consider the exact potential stability implications of trait object upcasting.

Potential full trait definitions
pub trait Integer: 'static
    // marker
    + Sized + Copy + Clone
    + Send + Sync + Unpin
    + UnwindSafe + RefUnwindSafe
    // fmt
    + Debug + Display + fmt::Binary + fmt::Octal
    + fmt::LowerHex + fmt::UpperHex + fmt::LowerExp + fmt::UpperExp
    // misc
    + Default + PartialEq + Eq + PartialOrd + Ord + Hash
    // conv
    + FromStr
    + for<Int: Integer> (TryFrom<Int> + TryInto<Int>)
    + for<Flt: Float> TryInto<Flt>
    // arith ops
    + ops::Add<Self, Output = Self> + for<'a> ops::Add<&'a Self, Output = Self>
    + ops::Sub<Self, Output = Self> + for<'a> ops::Sub<&'a Self, Output = Self>
    + ops::Mul<Self, Output = Self> + for<'a> ops::Mul<&'a Self, Output = Self>
    + ops::Div<Self, Output = Self> + for<'a> ops::Div<&'a Self, Output = Self>
    + ops::Rem<Self, Output = Self> + for<'a> ops::Rem<&'a Self, Output = Self>
    + ops::AddAssign<Self> + for<'a> ops::AddAssign<&'a Self>
    + ops::SubAssign<Self> + for<'a> ops::SubAssign<&'a Self>
    + ops::MulAssign<Self> + for<'a> ops::MulAssign<&'a Self>
    + ops::DivAssign<Self> + for<'a> ops::DivAssign<&'a Self>
    + ops::RemAssign<Self> + for<'a> ops::RemAssign<&'a Self>
    // bit ops
    + ops::Not
    + ops::BitAnd<Self, Output = Self> + for<'a> ops::BitAnd<&'a Self, Output = Self>
    + ops::BitOr<Self, Output = Self> + for<'a> ops::BitOr<&'a Self, Output = Self>
    + ops::BitXor<Self, Output = Self> + for<'a> ops::BitXor<&'a Self, Output = Self>
    + for<Int: Integer> (ops::Shl<Int, Output = Self> + for<'a> ops::Shl<&'a Int, Output = Self>)
    + for<Int: Integer> (ops::Shr<Int, Output = Self> + for<'a> ops::Shr<&'a Int, Output = Self>)
    + ops::BitAndAssign<Self> + for<'a> ops::BitAndAssign<&'a Self>
    + ops::BitOrAssign<Self> + for<'a> ops::BitAndAssign<&'a Self>
    + ops::BitXorAssign<Self> + for<'a> ops::BitAndAssign<&'a Self>
    + for<Int: Integer> (ops::ShlAssign<Int> + for<'a> ops::ShlAssign<&'a Int>)
    + for<Int: Integer> (ops::ShrAssign<Int> + for<'a> ops::ShrAssign<&'a Int>)
    // iter
    + iter::Sum<Self> + for<'a> iter::Sum<&'a Self>
    + iter::Product<Self> + for<'a> iter::Product<&'a Self>
// (only include the where bound if it's automatically elaborated)
where
    for<'a, 'b> &'a Self: Sized
        // arith ops
        + ops::Add<&'b Self, Output = Self>
        + ops::Sub<&'b Self, Output = Self>
        + ops::Mul<&'b Self, Output = Self>
        + ops::Div<&'b Self, Output = Self>
        + ops::Rem<&'b Self, Output = Self>
        // bit ops
        + ops::Not<Output = Self>
        + ops::BitAnd<&'b Self, Output = Self>
        + ops::BitOr<&'b Self, Output = Self>
        + ops::BitXor<&'b Self, Output = Self>
        + for<Int: Integer> ops::Shl<&'b Int, Output = Self>
        + for<Int: Integer> ops::Shr<&'b Int, Output = Self>
{
    // Actually, the int_impl! and uint_impl! don't deduplicate a common
    // subset, so by the definition above, this would currently be empty.
    // (This gives a nicer rustdoc item order.)
    // In practice, this should encompass, ignoring unstable:

    const MIN: Self;
    const MAX: Self;
    const BITS: u32;

    fn from_str_radix(src: &str, radix: u32) -> Result<Self, ParseIntError>;

    fn count_ones(self) -> u32;
    fn count_zeros(self) -> u32;
    fn leading_zeros(self) -> u32;
    fn trailing_zeros(self) -> u32;
    fn leading_ones(self) -> u32;
    fn trailing_ones(self) -> u32;

    fn rotate_left(self, n: u32) -> Self;
    fn rotate_right(self, n: u32) -> Self;

    fn swap_bytes(self) -> Self;
    fn reverse_bits(self) -> Self;

    fn from_be(x: Self) -> Self;
    fn from_le(x: Self) -> Self;
    fn to_be(self) -> Self;
    fn to_le(self) -> Self;

    fn checked_add(self, rhs: Self) -> Option<Self>;
    fn checked_sub(self, rhs: Self) -> Option<Self>;
    fn checked_mul(self, rhs: Self) -> Option<Self>;
    fn checked_div(self, rhs: Self) -> Option<Self>;
    fn checked_div_euclid(self, rhs: Self) -> Option<Self>;
    fn checked_rem(self, rhs: Self) -> Option<Self>;
    fn checked_rem_euclid(self, rhs: Self) -> Option<Self>;
    fn checked_neg(self) -> Option<Self>;
    fn checked_shl(self) -> Option<Self>;
    fn checked_shr(self) -> Option<Self>;
    fn checked_pow(self, exp: u32) -> Option<Self>;

    fn saturating_add(self, rhs: Self) -> Option<Self>;
    fn saturating_sub(self, rhs: Self) -> Option<Self>;
    fn saturating_mul(self, rhs: Self) -> Option<Self>;
    fn saturating_div(self, rhs: Self) -> Option<Self>;
    fn saturating_pow(self, exp: u32) -> Option<Self>;

    fn wrapping_add(self, rhs: Self) -> Option<Self>;
    fn wrapping_sub(self, rhs: Self) -> Option<Self>;
    fn wrapping_mul(self, rhs: Self) -> Option<Self>;
    fn wrapping_div(self, rhs: Self) -> Option<Self>;
    fn wrapping_div_euclid(self, rhs: Self) -> Option<Self>;
    fn wrapping_rem(self, rhs: Self) -> Option<Self>;
    fn wrapping_rem_euclid(self, rhs: Self) -> Option<Self>;
    fn wrapping_neg(self) -> Option<Self>;
    fn wrapping_shl(self) -> Option<Self>;
    fn wrapping_shr(self) -> Option<Self>;
    fn wrapping_pow(self, exp: u32) -> Option<Self>;

    fn overflowing_add(self, rhs: Self) -> (Self, bool);
    fn overflowing_sub(self, rhs: Self) -> (Self, bool);
    fn overflowing_mul(self, rhs: Self) -> (Self, bool);
    fn overflowing_div(self, rhs: Self) -> (Self, bool);
    fn overflowing_div_euclid(self, rhs: Self) -> (Self, bool);
    fn overflowing_rem(self, rhs: Self) -> (Self, bool);
    fn overflowing_rem_euclid(self, rhs: Self) -> (Self, bool);
    fn overflowing_neg(self, rhs: Self) -> (Self, bool);
    fn overflowing_shl(self, rhs: Self) -> (Self, bool);
    fn overflowing_shr(self, rhs: Self) -> (Self, bool);
    fn overflowing_pow(self, exp: u32) -> (Self, bool);

    fn pow(self, exp: u32) -> Self;
    fn div_euclid(self, rhs: Self) -> Self;
    fn rem_euclid(self, rhs: Self) -> Self;
    fn div_floor(self, rhs: Self) -> Self;
    fn div_ceil(self, rhs: Self) -> Self;

    fn ilog(self, rhs: Self) -> u32;
    fn ilog2(self, rhs: Self) -> u32;
    fn ilog10(self, rhs: Self) -> u32;
    fn checked_ilog(self, rhs: Self) -> Option<u32>;
    fn checked_ilog2(self, rhs: Self) -> Option<u32>;
    fn checked_ilog10(self, rhs: Self) -> Option<u32>;

    fn min_value() -> Self;
    fn max_value() -> Self;
}

pub trait Float: 'static
    // marker
    + Sized + Copy + Clone
    + Send + Sync + Unpin
    + UnwindSafe + RefUnwindSafe
    // fmt
    + Debug + Display + fmt::LowerExp + fmt::UpperExp
    // misc
    + Default + PartialEq + PartialOrd
    // conv
    + FromStr + for<Int: Integer> TryFrom<Int>
    // arith ops
    + ops::Add<Self, Output = Self> + for<'a> ops::Add<&'a Self, Output = Self>
    + ops::Sub<Self, Output = Self> + for<'a> ops::Sub<&'a Self, Output = Self>
    + ops::Mul<Self, Output = Self> + for<'a> ops::Mul<&'a Self, Output = Self>
    + ops::Div<Self, Output = Self> + for<'a> ops::Div<&'a Self, Output = Self>
    + ops::Rem<Self, Output = Self> + for<'a> ops::Rem<&'a Self, Output = Self>
    + ops::Neg<Output = Self>,
    + ops::AddAssign<Self> + for<'a> ops::AddAssign<&'a Self>
    + ops::SubAssign<Self> + for<'a> ops::SubAssign<&'a Self>
    + ops::MulAssign<Self> + for<'a> ops::MulAssign<&'a Self>
    + ops::DivAssign<Self> + for<'a> ops::DivAssign<&'a Self>
    + ops::RemAssign<Self> + for<'a> ops::RemAssign<&'a Self>
    // iter
    + iter::Sum<Self> + for<'a> iter::Sum<&'a Self>
    + iter::Product<Self> + for<'a> iter::Product<&'a Self>
// (only include the where bound if it's automatically elaborated)
where
    for<'a, 'b> &'a Self: Sized
        // arith ops
        + ops::Add<&'b Self, Output = Self>
        + ops::Sub<&'b Self, Output = Self>
        + ops::Mul<&'b Self, Output = Self>
        + ops::Div<&'b Self, Output = Self>
        + ops::Rem<&'b Self, Output = Self>
        + ops::Neg<Output = Self>
{
    // Actually, the float impls aren't macro pasted, so by
    // the definition above, this would currently be empty.
    // In practice, the entire impl body is available for both.
    // I've not replicated it as doing so confers no benefit.
}


  1. It is most likely that such a trait would only be able to contribute ops trait bounds and the [to|from]_[n|b|l]e_bytes methods; there's no other current associated item overlap between {integer} and {float} types. Even MIN/MAX have slightly different semantics for the two type sets. β†©οΈŽ

23 Likes

str is not Clone nor Copy.

What happens if the performance issues are fixed and str becomes struct str([u8]);? What exactly is a primitive?

2 Likes

What exactly is the value of having a Primitive trait? Given that you don't think it's useful in bounds (I agree) it seems entirely unnecessary. Integer makes more sense to me.

5 Likes

Uint could be very useful as well

3 Likes

I like the proposal. But I think it should discuss how this interacts with the potential addition of arbitrary integer sizes like u42 or i17 which are now available in C23 AFAIK.

Ah whoops. You can tell I was working from funty which doesn't include str in their Fundamental trait.

I personally agree with aiming to eventually treat str as just struct str([u8]). The definition I used was simply the types which are (nongeneric and) documented under the Primitive Types header and exported in the core::primitive module. These are the types which are still defined even if you unstably opt out of the prelude.

The primary purpose is expository. It serves as a single sealing point for both Integer and Float as well as a clear indication that these traits are strictly for the primitive arithmetic types and shall never be implemented for any other types, nor should concern themselves with applicability to other non-primitive numeric types.

This is at least touched on by

The one small wrinkle is that the functionality provided should not prevent the addition of new primitive arithmetic types (e.g. f16, f128, u256) in the future, should those be desired.

Personally, I think Rust would likely follow the behavior of C here, and if we get bit-precise integers, that it would be exposed as something like struct BitInt<const BITS: u32> rather than an unbounded number of uN types.

The potential full trait listing contains the pseudosyntax Integer: for<Int: Integer> (TryFrom<Int> + TryInto<Int>); this would imply every integer type is TryFrom every other integer type, enabling conversions between all of the bit-precise integers, presuming they're considered as primitive integer types. Additionally, none of the functionality seems like it would be difficult to provide, at least up through BitInt<128>, where the impl can just be to mask the sufficiently large power-of-two int. (Such a limit mirrors C, where _BitInt(N) is only guaranteed to be supported up to the size of unsigned long long.)

C _BitInt types are all primitive types, not structs. the proposed Rust syntax is int<N>/uint<N> iirc. Clang compiles them to LLVM IR's i23-style integer types iirc.

Clang supports or shortly will support >1000-bit integers, they're just waiting on LLVM gaining bigint div/rem support, which happened recently.

2 Likes

LLVM getting bigint div/rem: [RFC] Add support for division of large _BitInt (builtins, SelectionDAG/GlobalISel, clang) - Runtimes - LLVM Discussion Forums

Clang enabling >128-bit integers on x86: βš™ D139170 [X86][clang] Lift _BitInt() supported max width.

Rust generic ints RFC: RFC: Generic integers by clarfonthey Β· Pull Request #2581 Β· rust-lang/rfcs Β· GitHub

2 Likes

From the title I expected this would propose including something like what num_traits crate provides, but Primitive seems entirely useless to me.

1 Like

Well the Primitive trait might or might not be useless, but the Integer and Float traits sound very useful to abstract over the various types.

5 Likes

Primitive seems like a very normal trait for rust to have, putting extra information and assumptions in a type instead of simply in a doc string somewhere. In the very least case, pub trait Integer: Primitive + ... allows for a very normal path of discovery for someone to follow to learn "what is a primitive in rust".

As another example, Eq defines no extra methods over PartialEq, only extra permissible assumptions.

4 Likes

What extra assumptions would Primitive allow you to make, concretely?

5 Likes

Prior art:

I'm also unclear on the value proposition of a Primitive trait.

I've generally considered it to be a positive thing about Rust that it has no strongly defined notion of "primitiveness", i.e. there is no language level difference between i32 and struct Int(i32). I associate the phrase "primitive type" with Java where there is a huge difference between int and Integer and understanding that implicit difference between primitive types and objects is absolutely essential to writing any non-trivial code. Rust isn't like that, and I don't see why "primitiveness" should be a useful property to anyone who merely wants to use Rust (as opposed to someone who wants to modify the compiler, who may well care about the difference between str and struct str([u8])).

If there is no use for Primitive as a bound, then (it seems to me) its primary purpose is to be a page in the documentation, essentially "list of compiler-implemented types" which could be done without an RFC.

13 Likes

If there's unease with having a trait Primitive, maybe the docs explaining the "sealed" nature of Integer and Float could be the primary content on the core:: primitive module docs? The current content there could be a subsection "Usage in Macros" or similar.

Personally, I'm not opposed to a Primitive trait. It links together the other traits in a type manner, not just a doc note to "follow the link to see the module docs for more details".

It's worth noting that sealed traits have been RFC accepted and will be implemented on nightly soon. So if the only purpose of Primitive is to seal the trait, it'll be unnecessary.

5 Likes

What about SIMD types, e.g. __m512d? Are those primitive as well or is there an analogous trait that would apply?

By any useful definition, I don't think so. They're guaranteed to match layout/ABI to the vendor intrinsics (by #[repr(simd)]) but they're defined as structs and not available unless imported.

Additionally, while it's possible to write code generic over vector width by treating scalar quantities as a vector of size one, scalars and vectors are still very different concepts.

I think it's fairly well established here that the Primitive trait causes more harm than help, and as such I have pared the RFC back to just Integer and Floating.

5 Likes

Does it make sense to consider an Arithmetic (or Numeric) trait? It’s a fairly natural ask to to abstract over both ints and floats (without bringing in num) even though their semantics differ regarding overflow.

7 Likes