<int_ty>::IS_SIGNED

I was writing some generic (macro) code...

let src_is_signed = <$x>::MIN < 0;
let dst_is_signed = <$y>::MIN < 0;

... and I get this warning, with no way to silence it:

warning: comparison is useless due to type limits
   --> src/lib.rs:160:37
    |
160 |                 let dst_is_signed = <$y>::MIN < 0;
    |                                     ^^^^^^^^^^^^^
...
195 | impl_int_generic!(u64: isize, usize);
    | ------------------------------------- in this macro invocation
    |
    = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

So, is time for <int_ty>::IS_SIGNED (in the vein of associated BITS constants), or perhaps an option to turn off this warning is enough?

2 Likes

You could use MIN.count_ones() > 0 which works without warning, and can be used in constant context.

Edit: Or MIN != 0.

Edit: I would still find IS_SIGNED cleaner and useful.

6 Likes

I would love to have IS_SIGNED, and various other constants and methods. I think they'd provide much more value if they were part of a sealed trait that numeric types implemented, though. That would make it easy to write generic code over numeric types, whether using macros or generic functions.

3 Likes

I also want a way to convert between an unsigned to signed variants of the same width in a fully generic way. Specifically, given u32 or i32, being able to get u32 or i32 specifically.

Are you implying that you'd prefer some traits from the num crate be uplifted to core?

Another possible workaround:

fn lt<T: PartialOrd>(a: T, b: T) -> bool {
    a < b
}
fn foo() {
    let is_signed: bool = lt(i32::MIN, 0);
}

…though this doesn't work in const context, yet.

Yes, absolutely, though I'm suggesting that at least at first it may make sense for them to be sealed to make it easier to extend them in the future.

1 Like

The num-traits crate seems to cater for two distinct needs:

  1. traits for generalizing code over primitives
  2. traits that can be implemented for user types

Item 1 above is better served with sealed traits, as these can be updated with all the new features. However, sealing will block item 2. It seems to me that it is not possible to satisfy both needs with the same traits. I'd prefer sealed traits that are kept up to date in the standard library.

Exactly. I feel like the desire to make (2) perfect and handle backwards compatibility has prevented doing (1) in the standard library.

Adding a required method or type or constant to a non-sealed trait is a breaking change. But we can extend a sealed trait.

So, I'd favor putting a few sealed numeric traits in the standard library, whose primary purpose is to abstract over primitive types, and especially to abstract over things that are currently only available via intrinsic methods of numerics (and thus to macros but not generic functions).

None of that should supplant the third-party crate ecosystem, which can provide non-sealed numeric traits for user-defined numeric types.

9 Likes

Obviously a more official / far less "hacky" solution would be preferable, but if you need this functionality right now and are able to use nightly feature flags, it's not actually difficult to implement in a way that even works in const contexts:

#![feature(const_type_name)]

use std::any::type_name;

// By directly calling `type_name` here we guarantee that the names
// remain "up to date".
const I8_NAME: &str = type_name::<i8>();
const I16_NAME: &str = type_name::<i16>();
const I32_NAME: &str = type_name::<i32>();
const I64_NAME: &str = type_name::<i64>();
const I128_NAME: &str = type_name::<i128>();
const ISIZE_NAME: &str = type_name::<isize>();
const F32_NAME: &str = type_name::<f32>();
const F64_NAME: &str = type_name::<f64>();
const SIGNED_TYPES: [&str; 8] = [
    I8_NAME, I16_NAME, I32_NAME, I64_NAME,
    I128_NAME, ISIZE_NAME, F32_NAME, F64_NAME,
];

const fn const_cmp_str(lhs: &str, rhs: &str) -> bool {
    // Obviously the below code only makes sense in the context of
    // calling it at compile-time with constant input for both `lhs`
    // and `rhs`.
    let lhs_bytes = lhs.as_bytes();
    let rhs_bytes = rhs.as_bytes();
    if lhs_bytes.len() != rhs_bytes.len() {
        return false;
    }
    let mut i = 0;
    while i < lhs_bytes.len() {
        if lhs_bytes[i] != rhs_bytes[i] {
            return false;
        }
        i += 1;
    }
    true
}

const fn is_signed_internal<T: ?Sized>() -> bool {
    let mut i = 0;
    // `for x in y` does not work in const contexts yet.
    while i < SIGNED_TYPES.len() {
        // Can't do `SIGNED_TYPES[i] == type_name::<T>()` and have this
        // be `const`.
        if const_cmp_str(SIGNED_TYPES[i], type_name::<T>()) {
            return true;
        }
        i += 1;
    }
    false
}

#[inline(always)]
pub const fn is_signed<T: ?Sized>() -> bool {
    is_signed_internal::<T>()
}

#[inline(always)]
pub const fn is_signed_value<T: ?Sized>(_val: &T) -> bool {
    is_signed_internal::<T>()
}

const B1: bool = is_signed::<i32>();
const B2: bool = is_signed_value(&12i128);
const B3: bool = is_signed::<f64>();
const B4: bool = is_signed_value(&12.24599);
const B5: bool = is_signed::<u32>();
const B6: bool = is_signed_value(&900u32);
const B7: bool = is_signed::<u16>();
const B8: bool = is_signed_value("hello");

fn main() {
    println!(
        "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
        B1, B2, B3, B4, B5, B6, B7, B8
    );
}

Agreed, a sealed trait in std would be nice. But what should it contain besides IS_SIGNED?

Moving things like i32::MIN or i32::leading_zeros() to a new trait is a breaking change (even if the new trait is in the prelude due to inference in the case a user trait implements consts/methods of the same names on the same types).

Having those methods and consts defined on the types and on the new sealed trait should work I guess.

Yes, the inherent versions would have to remain as well, for compatibility.

By the way, I get ideas form irlo/PRs all the time and implement them in the fixed crate, as there I don't need to be as careful as the standard library in adding new features.

One thing I've had since 0.4 back in 2019 is three sealed traits for a generic way to access all the methods. The Fixed trait is implemented for all fixed-point numbers, the FixedSigned trait is implemented for all signed numbers (and has Fixed as a supertrait), and FixedUnsigned is implemented for all unsigned numbers (with Fixed as a supertrait). Since they're sealed, I add most new inherent methods and associated constants in the appropriate trait of the three.

Now I've just added IS_SIGNED. I added it as both an inherent associated constant, so that users do not have to import the trait; and as a trait associated constant, so that it can be used in generic code. I don't seem to have any issues yet because of adding methods/constants both in the inherent impl and in the traits.