platform-cast 0.1.0

Platform-specific safe cast.
Documentation
#![no_std]
//! Platform-specific safe cast.
//!
//! See [`CastFrom`] for details.

/// Like [`From`] but also support safe casts from/to [`usize`] and [`isize`].
///
/// |                           | [`From`] | [`TryFrom`] | [`as`][numeric cast] cast | [`CastFrom`] |
/// | ------------------------- | -------- | ----------- | ------------------------- | ------------ |
/// | No runtime check          | ✅       | ❌          | ✅                        | ✅           |
/// | [`usize`] <-> [`u64`][^a] | ❌       | ✅          | ✅                        | ✅           |
/// | No truncation             | ✅       | ✅          | ❌                        | ✅           |
/// | No change of sign         | ✅       | ✅          | ❌                        | ✅           |
///
/// [^a]: Assuming a target with a pointer width of 64.
///
/// [`From<usize>`] is not implemented for [`u64`] even on platforms
/// where the pointer width is 64 bit or lower (all currently supported
/// targets).
///
/// Thus the following code fails to compile:
/// ```compile_fail
/// let a = usize::MAX;
/// let b = u64::from(a);
/// // Error: the trait `From<usize>` is not implemented for `u64`
/// ```
///
/// Instead you have to resort to using [`TryFrom`] and
/// [`unwrap`](Option::unwrap):
/// ```
/// let a = usize::MAX;
/// let b = u64::try_from(a).unwrap();
/// ```
///
/// or using a potentially truncating/signedness-changing [numeric cast] using
/// `as`:
/// ```
/// let a = usize::MAX;
/// let b = a as u64;
/// ```
///
/// If you want to guarantee that there are no runtime panics and the value will
/// not be truncated, you can instead use [`CastFrom`] or [`CastInto`]:
/// ```
/// use platform_cast::{CastFrom, CastInto};
///
/// let a = usize::MAX;
/// let b = u64::cast_from(a);
///
/// // Alternatively:
/// let b: u64 = a.cast_into();
/// ```
///
/// The casts available depends on the target platform.
/// The following code will only compile if the target pointer width is 64 bit
/// or more.
/// ```ignore
/// use platform_cast::CastFrom;
///
/// let a = u64::MAX;
/// let b = usize::cast_from(a);
/// // On 32-bit targets: error[E0277]: the trait bound `usize: CastFrom<u64>`
/// is not satisfied
/// ```
///
/// [numeric cast]: https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#r-expr.as.numeric
pub trait CastFrom<T>: Sized {
    fn cast_from(value: T) -> Self;
}

/// Like [`Into`] but also support safe casts from/to [`usize`] and [`isize`].
///
/// See [`CastFrom`] for more details.
pub trait CastInto<T>: Sized {
    fn cast_into(self) -> T;
}

impl<T, U> CastInto<U> for T
where
    U: CastFrom<T>,
{
    fn cast_into(self) -> U {
        CastFrom::cast_from(self)
    }
}

macro_rules! nz {
    (u8) => {
        core::num::NonZeroU8
    };
    (i8) => {
        core::num::NonZeroI8
    };
    (u16) => {
        core::num::NonZeroU16
    };
    (i16) => {
        core::num::NonZeroI16
    };
    (u32) => {
        core::num::NonZeroU32
    };
    (i32) => {
        core::num::NonZeroI32
    };
    (u64) => {
        core::num::NonZeroU64
    };
    (i64) => {
        core::num::NonZeroI64
    };
    (u128) => {
        core::num::NonZeroU128
    };
    (i128) => {
        core::num::NonZeroI128
    };
    (usize) => {
        core::num::NonZeroUsize
    };
    (isize) => {
        core::num::NonZeroIsize
    };
}

macro_rules! cast_from {
    ($from:tt, $to:tt) => {
        const _: () = {
            let unsigned_from = <$from>::MIN == 0;
            let unsigned_to = <$to>::MIN == 0;
            if unsigned_from != unsigned_to {
                panic!("signedness differ");
            }

            #[allow(clippy::cast_possible_truncation)]
            if <$from>::MAX as $to as $from != <$from>::MAX {
                panic!("from::MAX doesn't fit in to");
            }
            #[allow(clippy::cast_possible_truncation)]
            if <$from>::MIN as $to as $from != <$from>::MIN {
                panic!("from::MIN doesn't fit in to");
            }
        };

        impl super::CastFrom<$from> for $to {
            #[allow(clippy::cast_possible_truncation)]
            fn cast_from(from: $from) -> $to {
                from as $to
            }
        }

        impl super::CastFrom<nz!($from)> for nz!($to) {
            fn cast_from(from: nz!($from)) -> nz!($to) {
                // SAFETY: As the input is non-zero the output is also non-zero
                unsafe { <nz!($to)>::new_unchecked(super::CastFrom::cast_from(from.get())) }
            }
        }
    };
}

#[cfg(target_pointer_width = "16")]
mod target16 {
    cast_from!(u8, usize);
    cast_from!(i8, isize);
    cast_from!(u16, usize);
    cast_from!(i16, isize);

    cast_from!(usize, u16);
    cast_from!(isize, i16);
    cast_from!(usize, u32);
    cast_from!(isize, i32);
    cast_from!(usize, u64);
    cast_from!(isize, i64);
    cast_from!(usize, u128);
    cast_from!(isize, i128);
}

#[cfg(target_pointer_width = "32")]
mod target32 {
    cast_from!(u8, usize);
    cast_from!(i8, isize);
    cast_from!(u16, usize);
    cast_from!(i16, isize);
    cast_from!(u32, usize);
    cast_from!(i32, isize);

    cast_from!(usize, u32);
    cast_from!(isize, i32);
    cast_from!(usize, u64);
    cast_from!(isize, i64);
    cast_from!(usize, u128);
    cast_from!(isize, i128);
}

#[cfg(target_pointer_width = "64")]
mod target64 {
    cast_from!(u8, usize);
    cast_from!(i8, isize);
    cast_from!(u16, usize);
    cast_from!(i16, isize);
    cast_from!(u32, usize);
    cast_from!(i32, isize);
    cast_from!(u64, usize);
    cast_from!(i64, isize);

    cast_from!(usize, u64);
    cast_from!(isize, i64);
    cast_from!(usize, u128);
    cast_from!(isize, i128);
}