diff --git a/enum-iterator-derive/Cargo.toml b/enum-iterator-derive/Cargo.toml index 5251156..7e02c49 100644 --- a/enum-iterator-derive/Cargo.toml +++ b/enum-iterator-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enum-iterator-derive" -version = "1.0.0" +version = "1.1.0" authors = ["Stephane Raux "] edition = "2021" description = "Procedural macro to derive Sequence" @@ -14,6 +14,6 @@ keywords = ["enum", "variants", "iterator", "enumerate", "cardinality"] proc-macro = true [dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "1", features = ["extra-traits"] } +proc-macro2 = "1.0.39" +quote = "1.0.18" +syn = { version = "1.0.96", features = ["extra-traits"] } diff --git a/enum-iterator-derive/src/lib.rs b/enum-iterator-derive/src/lib.rs index cc1e574..bdb309f 100644 --- a/enum-iterator-derive/src/lib.rs +++ b/enum-iterator-derive/src/lib.rs @@ -80,6 +80,7 @@ fn derive_for_struct( }; let tokens = quote! { impl #impl_generics ::enum_iterator::Sequence for #ty #ty_generics #where_clause { + #[allow(clippy::identity_op)] const CARDINALITY: usize = #cardinality; fn next(&self) -> ::core::option::Option { @@ -108,10 +109,8 @@ fn derive_for_enum( variants: &Punctuated, ) -> Result { let cardinality = enum_cardinality(variants); - let first = init_enum(ty, variants, Direction::Forward); - let last = init_enum(ty, variants.iter().rev(), Direction::Backward); let next_body = advance_enum(ty, variants, Direction::Forward); - let previous_body = advance_enum(ty, variants.iter().rev(), Direction::Backward); + let previous_body = advance_enum(ty, variants, Direction::Backward); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let where_clause = if generics.params.is_empty() { where_clause.cloned() @@ -128,8 +127,23 @@ fn derive_for_enum( ); Some(clause) }; + let next_variant_body = next_variant(ty, variants, Direction::Forward); + let previous_variant_body = next_variant(ty, variants, Direction::Backward); + let (first, last) = if variants.is_empty() { + ( + quote! { ::core::option::Option::None }, + quote! { ::core::option::Option::None }, + ) + } else { + let last_index = variants.len() - 1; + ( + quote! { next_variant(0) }, + quote! { previous_variant(#last_index) }, + ) + }; let tokens = quote! { impl #impl_generics ::enum_iterator::Sequence for #ty #ty_generics #where_clause { + #[allow(clippy::identity_op)] const CARDINALITY: usize = #cardinality; fn next(&self) -> ::core::option::Option { @@ -141,13 +155,28 @@ fn derive_for_enum( } fn first() -> ::core::option::Option { - ::core::option::Option::Some(#first) + #first } fn last() -> ::core::option::Option { - ::core::option::Option::Some(#last) + #last } } + + fn next_variant #impl_generics( + mut i: usize, + ) -> ::core::option::Option<#ty #ty_generics> #where_clause { + #next_variant_body + } + + fn previous_variant #impl_generics( + mut i: usize, + ) -> ::core::option::Option<#ty #ty_generics> #where_clause { + #previous_variant_body + } + }; + let tokens = quote! { + const _: () = { #tokens }; }; Ok(tokens) } @@ -194,22 +223,40 @@ fn init_fields(fields: &Fields, direction: Direction) -> TokenStream { .collect::() } -fn init_enum<'a, V>(ty: &Ident, variants: V, direction: Direction) -> TokenStream -where - V: IntoIterator, -{ - let inits = variants.into_iter().map(|variant| { - let id = &variant.ident; - let init = init_fields(&variant.fields, direction); +fn next_variant( + ty: &Ident, + variants: &Punctuated, + direction: Direction, +) -> TokenStream { + let advance = match direction { + Direction::Forward => { + let last_index = variants.len().saturating_sub(1); + quote! { + if i >= #last_index { break ::core::option::Option::None; } else { i+= 1; } + } + } + Direction::Backward => quote! { + if i == 0 { break ::core::option::Option::None; } else { i -= 1; } + }, + }; + let arms = variants.iter().enumerate().map(|(i, v)| { + let id = &v.ident; + let init = init_fields(&v.fields, direction); quote! { - #ty::#id { #init } + #i => ::core::option::Option::Some(#ty::#id { #init }) } }); quote! { - ::core::option::Option::None - #( - .or_else(|| ::core::option::Option::Some(#inits)) - )*? + loop { + let next = (|| match i { + #(#arms,)* + _ => ::core::option::Option::None, + })(); + match next { + ::core::option::Option::Some(_) => break next, + ::core::option::Option::None => #advance, + } + } } } @@ -220,39 +267,60 @@ fn advance_struct(ty: &Ident, fields: &Fields, direction: Direction) -> TokenStr quote! { let #ty { #assignments } = self; let (#(#bindings,)*) = #tuple?; - Some(#ty { #assignments }) + ::core::option::Option::Some(#ty { #assignments }) } } -fn advance_enum<'a, V>(ty: &Ident, variants: V, direction: Direction) -> TokenStream -where - V: IntoIterator, - V::IntoIter: Clone, -{ - let mut variants = variants.into_iter(); - let arms = iter::from_fn(|| variants.next().map(|variant| (variant, variants.clone()))).map( - |(variant, next_variants)| { - let next = init_enum(ty, next_variants, direction); - let id = &variant.ident; - let assignments = field_assignments(&variant.fields); - let bindings = bindings().take(variant.fields.len()).collect::>(); - let tuple = advance_tuple(&bindings, direction); - quote! { - #ty::#id { #assignments } => { - #tuple - .map(|(#(#bindings,)*)| #ty::#id { #assignments }) - .or_else(|| ::core::option::Option::Some(#next)) - } - } - }, - ); +fn advance_enum( + ty: &Ident, + variants: &Punctuated, + direction: Direction, +) -> TokenStream { + let arms: Vec<_> = match direction { + Direction::Forward => variants + .iter() + .enumerate() + .map(|(i, variant)| advance_enum_arm(ty, direction, i, variant)) + .collect(), + Direction::Backward => variants + .iter() + .enumerate() + .rev() + .map(|(i, variant)| advance_enum_arm(ty, direction, i, variant)) + .collect(), + }; quote! { - match self { + match *self { #(#arms,)* } } } +fn advance_enum_arm(ty: &Ident, direction: Direction, i: usize, variant: &Variant) -> TokenStream { + let next = match direction { + Direction::Forward => match i.checked_add(1) { + Some(next_i) => quote! { .or_else(|| next_variant(#next_i)) }, + None => quote! {}, + }, + Direction::Backward => match i.checked_sub(1) { + Some(prev_i) => quote! { .or_else(|| previous_variant(#prev_i)) }, + None => quote! {}, + }, + }; + let id = &variant.ident; + let destructuring = field_bindings(&variant.fields); + let assignments = field_assignments(&variant.fields); + let bindings = bindings().take(variant.fields.len()).collect::>(); + let tuple = advance_tuple(&bindings, direction); + quote! { + #ty::#id { #destructuring } => { + #tuple + .map(|(#(#bindings,)*)| #ty::#id { #assignments }) + #next + } + } +} + fn advance_tuple(bindings: &[Ident], direction: Direction) -> TokenStream { let advance = direction.advance(); let reset = direction.reset(); @@ -284,7 +352,7 @@ fn advance_tuple(bindings: &[Ident], direction: Direction) -> TokenStream { (::core::clone::Clone::clone(#rev_binding_tail), false) }; )* - Some((#(#bindings,)*)).filter(|_| !carry) + ::core::option::Option::Some((#(#bindings,)*)).filter(|_| !carry) }; quote! { (|| { #body })() @@ -306,6 +374,21 @@ where .collect() } +fn field_bindings<'a, I>(fields: I) -> TokenStream +where + I: IntoIterator, +{ + fields + .into_iter() + .enumerate() + .zip(bindings()) + .map(|((i, field), binding)| { + let field_id = field_id(field, i); + quote! { #field_id: ref #binding, } + }) + .collect() +} + fn bindings() -> impl Iterator { (0..).map(|i| Ident::new(&format!("x{i}"), Span::call_site())) } diff --git a/enum-iterator/Cargo.toml b/enum-iterator/Cargo.toml index 3ce7ace..63465f5 100644 --- a/enum-iterator/Cargo.toml +++ b/enum-iterator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enum-iterator" -version = "1.1.1" +version = "1.2.0" authors = ["Stephane Raux "] edition = "2021" description = "Tools to iterate over all values of a type (e.g. all variants of an enumeration)" @@ -11,4 +11,4 @@ documentation = "https://docs.rs/enum-iterator" keywords = ["enum", "variants", "iterator", "enumerate", "cardinality"] [dependencies] -enum-iterator-derive = {path = "../enum-iterator-derive", version = "1"} +enum-iterator-derive = { path = "../enum-iterator-derive", version = "1.1.0" } diff --git a/enum-iterator/README.md b/enum-iterator/README.md index 8013095..54b0c51 100644 --- a/enum-iterator/README.md +++ b/enum-iterator/README.md @@ -53,6 +53,10 @@ assert_eq!(first::(), Some(Foo { a: false, b: 0 })); assert_eq!(last::(), Some(Foo { a: true, b: 255 })); ``` +# Rust version +This crate tracks stable Rust. Minor releases may require a newer Rust version. Patch releases +must not require a newer Rust version. + # Contribute All contributions shall be licensed under the [0BSD license](https://spdx.org/licenses/0BSD.html). diff --git a/enum-iterator/src/lib.rs b/enum-iterator/src/lib.rs index c1ea094..eaf7dff 100644 --- a/enum-iterator/src/lib.rs +++ b/enum-iterator/src/lib.rs @@ -53,6 +53,10 @@ //! assert_eq!(last::(), Some(Foo { a: true, b: 255 })); //! ``` //! +//! # Rust version +//! This crate tracks stable Rust. Minor releases may require a newer Rust version. Patch releases +//! must not require a newer Rust version. +//! //! # Contribute //! All contributions shall be licensed under the [0BSD license](https://spdx.org/licenses/0BSD.html). @@ -60,7 +64,7 @@ #![deny(warnings)] #![no_std] -use core::iter::FusedIterator; +use core::{iter::FusedIterator, ops::ControlFlow}; pub use enum_iterator_derive::Sequence; @@ -523,6 +527,69 @@ impl Sequence for Option { } } +impl Sequence for [T; N] { + const CARDINALITY: usize = { + let tc = T::CARDINALITY; + let mut c = 1; + let mut i = 0; + loop { + if i == N { + break c; + } + c *= tc; + i += 1; + } + }; + + fn next(&self) -> Option { + advance_for_array(self, T::first) + } + + fn previous(&self) -> Option { + advance_for_array(self, T::last) + } + + fn first() -> Option { + if N == 0 { + Some(core::array::from_fn(|_| unreachable!())) + } else { + let x = T::first()?; + Some(core::array::from_fn(|_| x.clone())) + } + } + + fn last() -> Option { + if N == 0 { + Some(core::array::from_fn(|_| unreachable!())) + } else { + let x = T::last()?; + Some(core::array::from_fn(|_| x.clone())) + } + } +} + +fn advance_for_array(a: &[T; N], reset: R) -> Option<[T; N]> +where + T: Sequence + Clone, + R: Fn() -> Option, +{ + let mut a = a.clone(); + let keep = a.iter_mut().rev().try_fold((), |_, x| match x.next() { + Some(new_x) => { + *x = new_x; + ControlFlow::Break(true) + } + None => match reset() { + Some(new_x) => { + *x = new_x; + ControlFlow::Continue(()) + } + None => ControlFlow::Break(false), + }, + }); + Some(a).filter(|_| matches!(keep, ControlFlow::Break(true))) +} + macro_rules! impl_seq_advance_for_tuple { ( $this:ident, @@ -774,7 +841,7 @@ mod tests { } #[test] - fn check_option_items() { + fn all_bool_option_items_are_yielded() { assert!(all::>().eq([None, Some(false), Some(true)])); } @@ -789,4 +856,59 @@ mod tests { (Some(true), true), ])); } + + #[test] + fn cardinality_of_empty_array_is_one() { + assert_eq!(cardinality::<[u8; 0]>(), 1); + } + + #[test] + fn cardinality_equals_item_count_for_empty_array() { + cardinality_equals_item_count::<[u8; 0]>(); + } + + #[test] + fn cardinality_equals_item_count_for_array() { + cardinality_equals_item_count::<[u8; 3]>(); + } + + #[test] + fn array_items_vary_from_right_to_left() { + assert!(all::<[Option; 2]>().eq([ + [None, None], + [None, Some(false)], + [None, Some(true)], + [Some(false), None], + [Some(false), Some(false)], + [Some(false), Some(true)], + [Some(true), None], + [Some(true), Some(false)], + [Some(true), Some(true)], + ])); + } + + #[test] + fn all_empty_array_items_are_yielded() { + assert!(all::<[bool; 0]>().eq([[]])); + } + + #[test] + fn cardinality_of_empty_infallible_array_is_one() { + assert_eq!(cardinality::<[Infallible; 0]>(), 1); + } + + #[test] + fn cardinality_of_non_empty_infallible_array_is_zero() { + assert_eq!(cardinality::<[Infallible; 1]>(), 0); + } + + #[test] + fn all_empty_infallible_array_items_are_yielded() { + assert!(all::<[Infallible; 0]>().eq([[]])); + } + + #[test] + fn all_non_empty_infallible_array_items_are_yielded() { + assert!(all::<[Infallible; 1]>().next().is_none()); + } } diff --git a/enum-iterator/tests/derive.rs b/enum-iterator/tests/derive.rs index 6454bf8..73b6e60 100644 --- a/enum-iterator/tests/derive.rs +++ b/enum-iterator/tests/derive.rs @@ -145,3 +145,21 @@ fn reverse_all_works() { }; assert_eq!(reverse_all::().collect::>(), expected); } + +#[derive(Debug, PartialEq, Sequence)] +enum Empty {} + +#[test] +fn empty_cadinality_is_zero() { + assert_eq!(cardinality::(), 0); +} + +#[test] +fn all_values_of_empty_are_yielded() { + assert_eq!(all::().collect::>(), Vec::new()); +} + +#[test] +fn all_values_of_empty_are_yielded_in_reverse() { + assert_eq!(reverse_all::().collect::>(), Vec::new()); +}