rust-lang / rust-lang/libs-team

ACP: primitive numeric traits

Open
#371 16 comments 16 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api-change-proposal S-blocked
Dominant language
Rust
Stars
178
Forks
28
Avg merge
15m
Merged PRs (30d)
1

Description

Proposal

Problem statement

Rust developers, especially those coming from a C++-ish background, often want to write code which is generic over the different primitive numeric types (i.e. iNN, uNN, fNN). That this can't be done without using macro_rules! to paste a trait implementation across has been called "inelegant," and not uncommonly results in beginners trying what seems like an "obvious" axis for generalization being told "you can't do that" for reasons unrelated to what they're trying to accomplish.

The various ecosystem crates for numeric abstraction (e.g. num-traits, funty) clearly illustrate that the desire exists. This ACP doesn't aim to be the final solution, but offers a conservative step forward to solve a clearly defined subset of the problem that will remain useful even if a more general abstraction is added in the future.

Solution sketch

Specifically, we add two sealed and fundamental traits to core::primitive which capture exactly the set of types in the pseudo-types {integer} and {float} (the types given to integer and float literals before type inference). By virtue of being sealed and restricting semantics to just the primitive numeric types, the API signature that should be available on the trait is relatively straightforward — the API which already already exists macro-pasted over each of the primitive numeric types. These traits can be expanded in future releases to expose added inherent functionality

Per this, listed below is just what functionality is simple to add right now and is intended to be further extended.

// mod core::primitive

#[sealed]
#[fundamental]
trait Integer:
    'static + sealed::Sealed
    // core::marker
    + Sized + Copy + Clone + Send + Sync + Unpin
    // core::panic
    + panic::UnwindSafe + panic::RefUnwindSafe
    // core::fmt
    + fmt::Debug + fmt::Display + fmt::Binary + fmt::Octal
    + fmt::LowerHex + fmt::UpperHex + fmt::LowerExp + fmt::UpperExp
    // core::default
    + Default
    // core::cmp
    + PartialEq + Eq + PartialOrd + Ord
    // core::hash
    + hash::Hash
    // core::str
    + crate::str::FromStr
    // core::convert
    + TryFrom<u8> + TryInto<u8>
    + TryFrom<u16> + TryInto<u16>
    + TryFrom<u32> + TryInto<u32>
    + TryFrom<u64> + TryInto<u64>
    + TryFrom<u128> + TryInto<u128>
    + TryFrom<usize> + TryInto<usize>
    + TryFrom<i8> + TryInto<i8>
    + TryFrom<i16> + TryInto<i16>
    + TryFrom<i32> + TryInto<i32>
    + TryFrom<i64> + TryInto<i64>
    + TryFrom<i128> + TryInto<i128>
    + TryFrom<isize> + TryInto<isize>
    // core::ops
    + ops::Add<Output = Self> + for<'a> ops::Add<&'a Self, Output = Self>
    + ops::Sub<Output = Self> + for<'a> ops::Sub<&'a Self, Output = Self>
    + ops::Mul<Output = Self> + for<'a> ops::Mul<&'a Self, Output = Self>
    + ops::Div<Output = Self> + for<'a> ops::Div<&'a Self, Output = Self>
    + ops::Rem<Output = Self> + for<'a> ops::Rem<&'a Self, Output = Self>
    + ops::AddAssign + for<'a> ops::AddAssign<&'a Self>
    + ops::SubAssign + for<'a> ops::SubAssign<&'a Self>
    + ops::MulAssign + for<'a> ops::MulAssign<&'a Self>
    + ops::DivAssign + for<'a> ops::DivAssign<&'a Self>
    + ops::RemAssign + for<'a> ops::RemAssign<&'a Self>
    + ops::Not<Output = Self>
    + ops::BitAnd<Output = Self> + for<'a> ops::BitAnd<&'a Self, Output = Self>
    + ops::BitOr<Output = Self> + for<'a> ops::BitOr<&'a Self, Output = Self>
    + ops::BitXor<Output = Self> + for<'a> ops::BitXor<&'a Self, Output = Self>
    + ops::BitAndAssign + for<'a> ops::BitAndAssign<&'a Self>
    + ops::BitOrAssign + for<'a> ops::BitOrAssign<&'a Self>
    + ops::BitXorAssign + for<'a> ops::BitXorAssign<&'a Self>
    + ops::Shl<u8, Output = Self> + for<'a> ops::Shl<&'a u8, Output = Self>
    + ops::Shl<u16, Output = Self> + for<'a> ops::Shl<&'a u16, Output = Self>
    + ops::Shl<u32, Output = Self> + for<'a> ops::Shl<&'a u32, Output = Self>
    + ops::Shl<u64, Output = Self> + for<'a> ops::Shl<&'a u64, Output = Self>
    + ops::Shl<u128, Output = Self> + for<'a> ops::Shl<&'a u128, Output = Self>
    + ops::Shl<usize, Output = Self> + for<'a> ops::Shl<&'a usize, Output = Self>
    + ops::Shl<i8, Output = Self> + for<'a> ops::Shl<&'a i8, Output = Self>
    + ops::Shl<i16, Output = Self> + for<'a> ops::Shl<&'a i16, Output = Self>
    + ops::Shl<i32, Output = Self> + for<'a> ops::Shl<&'a i32, Output = Self>
    + ops::Shl<i64, Output = Self> + for<'a> ops::Shl<&'a i64, Output = Self>
    + ops::Shl<i128, Output = Self> + for<'a> ops::Shl<&'a i128, Output = Self>
    + ops::Shl<isize, Output = Self> + for<'a> ops::Shl<&'a isize, Output = Self>
    + ops::Shl<Self, Output = Self> + for<'a> ops::Shl<&'a Self, Output = Self>
    + ops::Shr<u8, Output = Self> + for<'a> ops::Shr<&'a u8, Output = Self>
    + ops::Shr<u16, Output = Self> + for<'a> ops::Shr<&'a u16, Output = Self>
    + ops::Shr<u32, Output = Self> + for<'a> ops::Shr<&'a u32, Output = Self>
    + ops::Shr<u64, Output = Self> + for<'a> ops::Shr<&'a u64, Output = Self>
    + ops::Shr<u128, Output = Self> + for<'a> ops::Shr<&'a u128, Output = Self>
    + ops::Shr<usize, Output = Self> + for<'a> ops::Shr<&'a usize, Output = Self>
    + ops::Shr<i8, Output = Self> + for<'a> ops::Shr<&'a i8, Output = Self>
    + ops::Shr<i16, Output = Self> + for<'a> ops::Shr<&'a i16, Output = Self>
    + ops::Shr<i32, Output = Self> + for<'a> ops::Shr<&'a i32, Output = Self>
    + ops::Shr<i64, Output = Self> + for<'a> ops::Shr<&'a i64, Output = Self>
    + ops::Shr<i128, Output = Self> + for<'a> ops::Shr<&'a i128, Output = Self>
    + ops::Shr<isize, Output = Self> + for<'a> ops::Shr<&'a isize, Output = Self>
    + ops::Shr<Self, Output = Self> + for<'a> ops::Shr<&'a Self, Output = Self>
    + ops::ShlAssign<u8> + for<'a> ops::ShlAssign<&'a u8>
    + ops::ShlAssign<u16> + for<'a> ops::ShlAssign<&'a u16>
    + ops::ShlAssign<u32> + for<'a> ops::ShlAssign<&'a u32>
    + ops::ShlAssign<u64> + for<'a> ops::ShlAssign<&'a u64>
    + ops::ShlAssign<u128> + for<'a> ops::ShlAssign<&'a u128>
    + ops::ShlAssign<usize> + for<'a> ops::ShlAssign<&'a usize>
    + ops::ShlAssign<i8> + for<'a> ops::ShlAssign<&'a i8>
    + ops::ShlAssign<i16> + for<'a> ops::ShlAssign<&'a i16>
    + ops::ShlAssign<i32> + for<'a> ops::ShlAssign<&'a i32>
    + ops::ShlAssign<i64> + for<'a> ops::ShlAssign<&'a i64>
    + ops::ShlAssign<i128> + for<'a> ops::ShlAssign<&'a i128>
    + ops::ShlAssign<isize> + for<'a> ops::ShlAssign<&'a isize>
    + ops::ShlAssign<Self> + for<'a> ops::ShlAssign<&'a Self>
    + ops::ShrAssign<u8> + for<'a> ops::ShrAssign<&'a u8>
    + ops::ShrAssign<u16> + for<'a> ops::ShrAssign<&'a u16>
    + ops::ShrAssign<u32> + for<'a> ops::ShrAssign<&'a u32>
    + ops::ShrAssign<u64> + for<'a> ops::ShrAssign<&'a u64>
    + ops::ShrAssign<u128> + for<'a> ops::ShrAssign<&'a u128>
    + ops::ShrAssign<usize> + for<'a> ops::ShrAssign<&'a usize>
    + ops::ShrAssign<i8> + for<'a> ops::ShrAssign<&'a i8>
    + ops::ShrAssign<i16> + for<'a> ops::ShrAssign<&'a i16>
    + ops::ShrAssign<i32> + for<'a> ops::ShrAssign<&'a i32>
    + ops::ShrAssign<i64> + for<'a> ops::ShrAssign<&'a i64>
    + ops::ShrAssign<i128> + for<'a> ops::ShrAssign<&'a i128>
    + ops::ShrAssign<isize> + for<'a> ops::ShrAssign<&'a isize>
    + ops::ShrAssign<Self> + for<'a> ops::ShrAssign<&'a Self>
    // core::iter
    + iter::Sum<Self> + for<'a> iter::Sum<&'a Self>
    + iter::Product<Self> + for<'a> iter::Product<&'a Self>
{
    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>;

    unsafe fn unchecked_add(self, rhs: Self) -> Self;
    unsafe fn unchecked_sub(self, rhs: Self) -> Self;
    unsafe fn unchecked_mul(self, rhs: Self) -> Self;
    unsafe fn unchecked_shl(self, rhs: u32) -> Self;
    unsafe fn unchecked_shr(self, rhs: u32) -> Self;

    fn strict_add(self, rhs: Self) -> Self;
    fn strict_sub(self, rhs: Self) -> Self;
    fn strict_mul(self, rhs: Self) -> Self;
    fn strict_div(self, rhs: Self) -> Self;
    fn strict_div_euclid(self, rhs: Self) -> Self;
    fn strict_rem(self, rhs: Self) -> Self;
    fn strict_rem_euclid(self, rhs: Self) -> Self;
    fn strict_neg(self) -> Self;
    fn strict_shl(self, rhs: u32) -> Self;
    fn strict_shr(self, rhs: u32) -> Self;
    fn strict_pow(self, exp: u32) -> 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 isqrt(self) -> 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;
}

#[sealed]
#[fundamental]
pub trait Float:
    'static + sealed::Sealed
    // core::marker
    + Sized + Copy + Clone + Send + Sync + Unpin
    // core::panic
    + panic::UnwindSafe + panic::RefUnwindSafe
    // core::fmt
    + fmt::Debug // + fmt::Display + fmt::LowerExp + fmt::UpperExp
    // core::default
    + Default
    // core::cmp
    + PartialEq + PartialOrd
    // core::ops
    + ops::Add<Output = Self> + for<'a> ops::Add<&'a Self, Output = Self>
    + ops::Sub<Output = Self> + for<'a> ops::Sub<&'a Self, Output = Self>
    + ops::Mul<Output = Self> + for<'a> ops::Mul<&'a Self, Output = Self>
    + ops::Div<Output = Self> + for<'a> ops::Div<&'a Self, Output = Self>
    + ops::Rem<Output = Self> + for<'a> ops::Rem<&'a Self, Output = Self>
    + ops::AddAssign + for<'a> ops::AddAssign<&'a Self>
    + ops::SubAssign + for<'a> ops::SubAssign<&'a Self>
    + ops::MulAssign + for<'a> ops::MulAssign<&'a Self>
    + ops::DivAssign + for<'a> ops::DivAssign<&'a Self>
    + ops::RemAssign + for<'a> ops::RemAssign<&'a Self>
    + ops::Neg
{
    fn is_nan(self) -> bool;
    fn is_sign_positive(self) -> bool;
    fn is_sign_negative(self) -> bool;

    // note: this is limited to current partial f16/f128 support
}

In the future, an extension to coherence that would allow impl<T: Integer> Trait for T and impl<T: Float> Trait for F to be known non-overlapping (e.g. equivalent to using the impl headers for the fundamental trait bound) could make

Alternatives

Doing nothing in std and leaving it to ecosystem crates to provide this functionality is a valid option, and has worked so far.

Instead of using a single trait with both bounds and methods, it could be beneficial to put methods on one trait and expose Integer/Float as trait aliases which include the bound on method availability and all of the other traits. The trait alias form would be able to provide additional &Self: Op<&Self, Output = Self> bounds (while still getting them elaborated at use sites) as well as enable downstreams that want less ambiguous inference around e.g. shifts or conversions to specify fewer bounds.

Splitting methods/bounds has potential technical benefits, but I don't see splitting the functionality further (e.g. to extract a shared supertrait) as being particularly beneficial. Even additional SignedInteger/UnsignedInteger subtraits seem difficult to justify; the goal is not to create a robust modelling of algebraic fields (that would be delegated to ecosystem crates, e.g. num-traits, alga, or simba), but solely to enable the "obvious" generalization over the primitive numeric types that would otherwise have to be done with macros.

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the core::primitive entry point described in the proposal and review the requested Integer and Float trait surface against existing primitive-type APIs. No file or test is named; done would require an agreed scope and an implementation plan for the sealed, fundamental traits and their supported operations.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.