Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: const variants of into, from, try_from, and default #147

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 45 additions & 1 deletion num_enum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#![cfg_attr(not(feature = "std"), no_std)]

pub use ::num_enum_derive::{
Default, FromPrimitive, IntoPrimitive, TryFromPrimitive, UnsafeFromPrimitive,
ConstDefault, ConstFromPrimitive, ConstIntoPrimitive, ConstTryFromPrimitive, Default,
FromPrimitive, IntoPrimitive, TryFromPrimitive, UnsafeFromPrimitive,
};

use ::core::fmt;
Expand All @@ -28,6 +29,13 @@ pub trait TryFromPrimitive: Sized {
fn try_from_primitive(number: Self::Primitive) -> Result<Self, Self::Error>;
}

pub trait ConstTryFromPrimitive: Sized {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you feel about making ConstTryFromPrimitive a subtrait of TryFromPrimitive (and similarly for the others)? I think that would allow us to remove ConstTryFromPrimitiveError (we could just use TryFromPrimitiveError as the bound would still hold), and would mean the new const generic on EnumInfo could disappear?

I guess it's a bit annoying to have to derive both if you just want the const version, but I'm also not sure how common it would be to only want the const version, and also it mirrors how things like PartialEq/Eq work?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds great! It reminds me of what I was trying to do with ConstantTimePartialOrd in subtle a while ago (dalek-cryptography/subtle#98). I'll see if this works.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally do not have a use case for const-only derives -- I think I was a little confused about how const fn can also be a normal runtime fn and how to separate those. I think a subtrait and merging the errors (which I would definitely like to do) would be ideal -- will check that out now.

type Primitive: Copy + Eq + fmt::Debug;
type Error;

const NAME: &'static str;
}

pub trait UnsafeFromPrimitive: Sized {
type Primitive: Copy + Eq;

Expand Down Expand Up @@ -84,6 +92,38 @@ impl<Enum: TryFromPrimitive> fmt::Display for TryFromPrimitiveError<Enum> {
#[cfg(feature = "std")]
impl<Enum: TryFromPrimitive> ::std::error::Error for TryFromPrimitiveError<Enum> {}

#[derive(Copy, Clone, PartialEq, Eq)]
pub struct ConstTryFromPrimitiveError<Enum: ConstTryFromPrimitive> {
pub number: Enum::Primitive,
}

impl<Enum: ConstTryFromPrimitive> ConstTryFromPrimitiveError<Enum> {
pub const fn new(number: Enum::Primitive) -> Self {
Self { number }
}
}

impl<Enum: ConstTryFromPrimitive> fmt::Debug for ConstTryFromPrimitiveError<Enum> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("ConstTryFromPrimitiveError")
.field("number", &self.number)
.finish()
}
}
impl<Enum: ConstTryFromPrimitive> fmt::Display for ConstTryFromPrimitiveError<Enum> {
fn fmt(&self, stream: &'_ mut fmt::Formatter<'_>) -> fmt::Result {
write!(
stream,
"No discriminant in enum `{name}` matches the value `{input:?}`",
name = Enum::NAME,
input = self.number,
)
}
}

#[cfg(feature = "std")]
impl<Enum: ConstTryFromPrimitive> ::std::error::Error for ConstTryFromPrimitiveError<Enum> {}

// This trait exists to try to give a more clear error message when someone attempts to derive both FromPrimitive and TryFromPrimitive.
// This isn't allowed because both end up creating a `TryFrom<primitive>` implementation.
// TryFromPrimitive explicitly implements TryFrom<primitive> with Error=TryFromPrimitiveError, which conflicts with:
Expand All @@ -93,3 +133,7 @@ impl<Enum: TryFromPrimitive> ::std::error::Error for TryFromPrimitiveError<Enum>
// It is subject to change in any release regardless of semver.
#[doc(hidden)]
pub trait CannotDeriveBothFromPrimitiveAndTryFromPrimitive {}

// Similar, for const primitive coercion methods.
#[doc(hidden)]
pub trait CannotDeriveBothConstFromPrimitiveAndConstTryFromPrimitive {}
37 changes: 37 additions & 0 deletions num_enum/tests/const_default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#![allow(non_upper_case_globals)]

// Guard against https://github.com/illicitonion/num_enum/issues/27
mod alloc {}
mod core {}
mod num_enum {}
mod std {}

#[test]
fn default() {
#[derive(Debug, Eq, PartialEq, ::num_enum::ConstDefault)]
#[repr(u8)]
enum Enum {
#[allow(unused)]
Zero = 0,
#[num_enum(default)]
NonZero = 1,
}

const nz: Enum = Enum::const_default();
assert_eq!(Enum::NonZero, nz);
}

#[test]
fn default_standard_default_attribute() {
#[derive(Debug, Eq, PartialEq, ::num_enum::ConstDefault)]
#[repr(u8)]
enum Enum {
#[allow(unused)]
Zero = 0,
#[default]
NonZero = 1,
}

const nz: Enum = Enum::const_default();
assert_eq!(Enum::NonZero, nz);
}
116 changes: 116 additions & 0 deletions num_enum/tests/const_from_primitive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#![allow(non_upper_case_globals)]

use ::num_enum::ConstFromPrimitive;

// Guard against https://github.com/illicitonion/num_enum/issues/27
mod alloc {}
mod core {}
mod num_enum {}
mod std {}

macro_rules! has_from_primitive_number {
( $type:ty ) => {
paste::paste! {
#[test]
fn [<has_from_primitive_number_ $type>]() {
#[derive(Debug, Eq, PartialEq, ConstFromPrimitive)]
#[repr($type)]
enum Enum {
Zero = 0,
#[num_enum(default)]
NonZero = 1,
}

const zero: Enum = Enum::const_from(0 as $type);
assert_eq!(zero, Enum::Zero);

const one: Enum = Enum::const_from(1 as $type);
assert_eq!(one, Enum::NonZero);

const two: Enum = Enum::const_from(2 as $type);
assert_eq!(two, Enum::NonZero);
}
}
};
}

has_from_primitive_number!(u8);
has_from_primitive_number!(u16);
has_from_primitive_number!(u32);
has_from_primitive_number!(u64);
has_from_primitive_number!(usize);
has_from_primitive_number!(i8);
has_from_primitive_number!(i16);
has_from_primitive_number!(i32);
has_from_primitive_number!(i64);
has_from_primitive_number!(isize);
// repr with 128-bit type is unstable
// has_from_primitive_number!(u128);
// has_from_primitive_number!(i128);

#[test]
fn has_from_primitive_number_standard_default_attribute() {
#[derive(Debug, Eq, PartialEq, ConstFromPrimitive)]
#[repr(u8)]
enum Enum {
Zero = 0,
#[default]
NonZero = 1,
}

const zero: Enum = Enum::const_from(0_u8);
assert_eq!(zero, Enum::Zero);

const one: Enum = Enum::const_from(1_u8);
assert_eq!(one, Enum::NonZero);

const two: Enum = Enum::const_from(2_u8);
assert_eq!(two, Enum::NonZero);
}

#[test]
fn from_primitive_number_catch_all() {
#[derive(Debug, Eq, PartialEq, ConstFromPrimitive)]
#[repr(u8)]
enum Enum {
Zero = 0,
#[num_enum(catch_all)]
NonZero(u8),
}

const zero: Enum = Enum::const_from(0_u8);
assert_eq!(zero, Enum::Zero);

const one: Enum = Enum::const_from(1_u8);
assert_eq!(one, Enum::NonZero(1_u8));

const two: Enum = Enum::const_from(2_u8);
assert_eq!(two, Enum::NonZero(2_u8));
}

#[cfg(feature = "complex-expressions")]
#[test]
fn from_primitive_number_with_inclusive_range() {
#[derive(Debug, Eq, PartialEq, ConstFromPrimitive)]
#[repr(u8)]
enum Enum {
Zero = 0,
#[num_enum(alternatives = [2..=255])]
NonZero,
}

const zero: Enum = Enum::const_from(0_u8);
assert_eq!(zero, Enum::Zero);

const one: Enum = Enum::const_from(1_u8);
assert_eq!(one, Enum::NonZero);

const two: Enum = Enum::const_from(2_u8);
assert_eq!(two, Enum::NonZero);

const three: Enum = Enum::const_from(3_u8);
assert_eq!(three, Enum::NonZero);

const twofivefive: Enum = Enum::const_from(255_u8);
assert_eq!(twofivefive, Enum::NonZero);
}
52 changes: 52 additions & 0 deletions num_enum/tests/const_into_primitive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![allow(non_upper_case_globals)]

use ::num_enum::ConstIntoPrimitive;

// Guard against https://github.com/illicitonion/num_enum/issues/27
mod alloc {}
mod core {}
mod num_enum {}
mod std {}

#[derive(ConstIntoPrimitive)]
#[repr(u8)]
enum Enum {
Zero,
One,
Two,
}

#[test]
fn simple() {
const zero: u8 = Enum::Zero.const_into();
assert_eq!(zero, 0u8);
assert_eq!(zero, Enum::Zero.into());

const one: u8 = Enum::One.const_into();
assert_eq!(one, 1u8);
assert_eq!(one, Enum::One.into());

const two: u8 = Enum::Two.const_into();
assert_eq!(two, 2u8);
assert_eq!(two, Enum::Two.into());
}

#[test]
fn catch_all() {
#[derive(Debug, Eq, PartialEq, ConstIntoPrimitive)]
#[repr(u8)]
enum Enum {
Zero = 0,
#[num_enum(catch_all)]
NonZero(u8),
}

const zero: u8 = Enum::Zero.const_into();
assert_eq!(zero, 0u8);

const one: u8 = Enum::NonZero(1u8).const_into();
assert_eq!(one, 1u8);

const two: u8 = Enum::NonZero(2u8).const_into();
assert_eq!(two, 2u8);
}