rust-lang / libs-team

The home of the library team
Apache License 2.0
110 stars 18 forks source link

ACP: primitive numeric traits #371

Open CAD97 opened 2 months ago

CAD97 commented 2 months ago

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):

Second, if there's a concrete solution:

programmerjake commented 2 months ago

don't forget shifts by and conversions from/to usize/isize, additionally fmt::LowerExp + fmt::UpperExp should be on Float

CAD97 commented 2 months ago

Additional note: having these traits in the prelude (although probably as _) could resolve many cases of the "cannot call method on type {integer}" error, since name resolution would select the trait methods, similar to how literal type inference propagates through operators already. It could also incidentally resolve the odd behavior where x.f(); x.f(); can call two different methods if the first is resolved to a trait method that constrains the receiver to a single type such that the second is an inherent method call, by making the trait method resolution on {integer} either this method delegating to the inherent one or ambiguous.

workingjubilee commented 2 months ago

It seems non-intuitive to me to require all of these implementations together in a single trait. Mostly, I'm eyeing

It is my experience that these are the most likely to be inference-breaking in many cases. While T-libs-api is I am sure thinking about this sort of thing often, I think it would be good to discuss the logistics of bound-selection here, given that bounds get implied in some cases and not in others, which means in some cases relaxing a bound is in fact API-breaking, if memory serves. And I mention this because we may wish to add a novel impl Integer type to the language in the future, which would suddenly make all these implications relevant, especially if it has been added to the prelude.

Not "why these picks", I mean, more "what to keep in mind when making these picks".

CAD97 commented 2 months ago

don't forget [...] usize/isize

Added those to the ACP and the draft PR, as well as a few that were already available in the PR but not the ACP.

[...] should be on Float

The current Float definition is quite limited due to f16/f128 barely existing at the moment, and including support for them in the draft PR I made while collecting/validating this list.

non-intuitive to me to require all of these

I know you said you aren't asking "why" but the response is to clarify "why:" it's supposed to encompass all of the (elaboratable) bounds satisfied by all of the primitive numeric types and whose definition is width-independent, meaning that supplying the functionality for new uN/iN/fN types should be reasonably straightforward (even if not trivial). It's not as much require as provide because this trait cannot be implemented except in core. The specific purpose is to bundle all of these bounds into a single one.

That said, any of this API surface potentially not making sense to provide for a future uN/iN/fN type is valid reason to exclude it. I think all of the listed functionality we would want to provide on any new primitives, even if just for consistency. (E.g. I already excluded carrying_add for having subtly different implications for signed integers and making less sense on u256.)

Also note that I only included Self: Op<&Self, Output = Self> in the bounds list and not where &Self: Op<&Self, Output = Self> specifically to limit the bounds to ones elaborated from just T: Integer. I did add another alternative for using trait aliases to separate the bounds from the methods, though.

scottmcm commented 1 month ago

I wonder if, like with NonZero<_>, another way here would be to have u<N> as something that can be written, with support for all the methods on that.

That would allow handling the "I just wanted to implement it for the types" in a way that's directly allowed with coherence because u<N> is clearly not any user type, without needing to get into #[fundamental] stuff.

programmerjake commented 1 month ago

I wonder if, like with NonZero<_>, another way here would be to have u<N> as something that can be written, with support for all the methods on that.

like https://github.com/rust-lang/rfcs/pull/2581? I was just mentioning on Zulip that Rust is falling behind C/C++ which already have _BitInt(N) so LLVM already supports it (though there are some more ABI mistakes where __int128 has different alignment than _BitInt(128) that needs a specification fix...)

Amanieu commented 1 month ago

We discussed this in the libs-api meeting yesterday. The overall feeling was that it introduces a rather large API that duplicates the existing inherent methods and it's not clear why this specifically needs to be in the standard library when crates like num-traits already exist.

CAD97 commented 1 month ago

why in the standard library

Primarily, for coherence reasons.

Because the traits are marked #[fundamental], coherence knows that types don't implement the trait, and this works today. Ideally, we would also teach coherence to know that these trait bounds refer to a specific fixed set of types, making for<T: Integer> T and for<T: Floating> T knowably disjoint from each other. This is not something that can be provided for library traits; #[sealed] #[fundamental] isn't sufficient.

Secondarily, for optics reasons. These type sets are the most obvious ones to generalize code over, and the inability to write code generic over integer width without macros is not infrequently cited as a failing of Rust.

A solution that made uNN into a type generic over bit width instead of being separate types would address the same goals as this intends to in a way which would decrease the API size of std instead of increase it and is, tbh, a more principled direction. (It wouldn't cover usize, but that's an acceptable limitation imo.) This approach is simpler to do in the shorter term.

If libs-api thinks the language approach without the trait is a preferable direction to explore, then this can be closed in favor of that.

clarfonthey commented 1 month ago

Biased as the author of rust-lang/rfcs#2581, but I also would be largely in favour of having generic uint<N> and int<N> types (with usize and isize being separate as they are today) instead of having an integer trait. Although I know we don't do this currently, we could conceptually think of usize and isize as opaque newtypes over their respective underlying integer types, and it would make sense why they aren't included. And just from an API standpoint, usize and isize not being platform-dependent aliases is intentional.

Floats are… way more complicated and would be much less likely to support generics. Especially if we consider floating-point formats like f64f64 which are listed in rust-lang/rfcs#3456. The only way I could possibly see supporting it is if you had a non_exhaustive enum of FloatType, and then float<const F: FloatType> were the "generic" float type, which feels a bit unprecedented. I do wonder how likely this kind of pattern will be once it becomes possible on stable, however (with constant traits and arbitrary-type constant generics).

Perhaps a generic float<M, E> could exist where the float has a sign bit, M mantissa bits, and E exponent bits, using the standard rules for IEEE 754 like a bias derived from the exponent bits, NaN, infinity, and the various rounding rules. This would cover types like bf16 and f80 but would exclude "weird" types like f64f64, which is probably good enough.

I'm not trying to design the full feature here for floats, but want to express the likely path any design would take, so that it can be compared for complexity with a trait. For integers, the decision feels obvious to me, with a generic type being the better path, but the added complexity for floats certainly changes things on that camp. Additionally, if we do assume that all of the proposed float types get stabilised, it would put the number of them similar to the existing integer types, with f16, f32, f64, f128, bf16, and f80 (excluding f64f64, that's 6 types), compared to the integer i8, i16, i32, i64, and i128 (excluding isize, that's 5 types, and double that for unsigned).

We can also consider "weird" floating-point types like f64f64 equivalent to, say, offering one's complement integer types. Since these integer types work so differently from the existing ones, we wouldn't expect the same code to work for them because of this. The same could probably be said of f64f64: we expect a certain format even down to the bit level for all existing floats, with the number of bits in each field being what's different. If even that changes, I think you should be required to make separate code for it.

Something that would generalise over all float types including something like f64f64 definitely feels like territory covered by a crate like num-traits, where you want to generalise over operations like addition, multiplication, division, etc., but not the underlying format. (Speaking of which… can f64f64 have one field NaN and the other not? That would make even methods like is_nan not doable for those types.)

programmerjake commented 1 month ago

Biased as the author of rust-lang/rfcs#2581, but I also would be largely in favour of having generic uint<N> and int<N> types (with usize and isize being separate as they are today)

I'm all for having generic integer types, but I don't think that means we can't have integer traits too, since abstracting over signed/unsigned and usize/uint<N> is still useful, though less significant of an issue.

Perhaps a generic float<M, E> could exist where the float has a sign bit, M mantissa bits, and E exponent bits, using the standard rules for IEEE 754 like a bias derived from the exponent bits, NaN, infinity, and the various rounding rules. This would cover types like bf16 and f80

agreed, though f80 is also kinda weird since it has an explicit mantissa integer bit instead of an implicit bit like all other IEEE 754 types; still waay better than f64f64 though.

clarfonthey commented 1 month ago

I'm all for having generic integer types, but I don't think that means we can't have integer traits too, since abstracting over signed/unsigned and usize/uint<N> is still useful, though less significant of an issue.

Incredibly fair, although IMHO the question of whether you should implement a trait for usize when you implement it for uint<N>, and similarly for isize and int<N>, feels like the kind of API decision that downstream crates can decide upon. Additionally, I'd be more in favour of a trait to generalise integers in the sense of coercing from integer literals to types, rather than one that specifically just generalises over all their methods, since again, there are things that might be minutely different between them.

agreed, though f80 is also kinda weird since it has an explicit mantissa integer bit instead of an implicit bit like all other IEEE 754 types; still waay better than f64f64 though.

Oh, I actually had no idea this was the case. In that sense, it would probably still count as a "weird" float type, since it doesn't behave the same as the IEEE 754 types. IMHO, generalising over all these different variables on floats is exactly the kind of thing we'd like to avoid, since there are all kinds of parameters you could control like how the bias works, whether subnormal numbers exist, NaN behaviour, etc., but you really shouldn't, because floating-point semantics are already a mess and we should at least agree on what the bits mean.

programmerjake commented 1 month ago

agreed, though f80 is also kinda weird since it has an explicit mantissa integer bit instead of an implicit bit like all other IEEE 754 types; still waay better than f64f64 though.

Oh, I actually had no idea this was the case. In that sense, it would probably still count as a "weird" integer type, since it doesn't behave the same as the IEEE 754 types.

well, actually it does behave almost exactly like an IEEE 754 type with 1 sign bit, 15 exponent bits, and 63 mantissa bits, except that there's an extra bit that's usually 1 (0 for denormals), but you can have unnormalized mantissas, and NaNs use all 64-bits of the mantissa. so it very much is a floating-point type, not integer.

clarfonthey commented 1 month ago

Yeah, that was 100% just me misspeaking; please carry on.

The extra bit making the difference between how subnormals work is substantial enough a difference to make it different from other float types, IMHO.

CAD97 commented 1 month ago

Having had some more time to think about it, I'm now shifting much more firmly into thinking that turning int<N> and uint<N> into the primitive integer types (with iNN/uNN being aliases for specific widths) is likely the best way to address the need that the Integer side of this ACP is looking at.

IEEE defines what the binary{k} interchange format is ($k \geq 128$, $k \equiv 0 \mod 32$) in addition to {16, 32, 64, 128}, so I also think that float<N> (requiring N to be a valid interchange value) is a good choice for the Float side of the described need. (bf16, f80, f64f64 would not be included.)

Generalization to both signed and unsigned, to include usize/isize, and/or to include non-interchange floating point formats can be left to ecosystem traits to define. A future RFC defining traits permitting user-defined types to be constructed by numeric literals would also in significant part supersede the traits in this ACP, as an explicit part of their goal is to describe the set of types that the literals can be unified with.

I'm going to leave this open until there's an RFC to point to (or t-libs opts to close it) for the time being, though, since I still strongly believe the underlying need is legitimate, even if this specific API isn't the best way to address it.