diff --git a/Cargo.toml b/Cargo.toml index e7f5165..06b767f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ debug = [] full = [ "eq", "debug" ] [dependencies] -thiserror = "2.0" -thisenum-impl = { version = "0.2.2", path = "impl" } +thiserror = "~2.0" +thisenum-impl = { version = "0.2.1", path = "impl" } [dev-dependencies] diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 39b248f..087c8b3 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -20,12 +20,12 @@ full = [ "eq", "debug" ] proc-macro = true [dependencies] -syn = "1.0" -paste = "1.0" -quote = "1.0" -unzip-n = "0.1" -thiserror = "1.0" -proc-macro2 = "1.0" +paste = "~1.0" +quote = "~1.0" +unzip-n = "~0.1" +thiserror = "~2.0" +proc-macro2 = "~1.0" +syn = { version = "~2.0", features = ["full", "extra-traits"] } [dev-dependencies] diff --git a/impl/src/ast/container.rs b/impl/src/ast/container.rs new file mode 100644 index 0000000..28151d7 --- /dev/null +++ b/impl/src/ast/container.rs @@ -0,0 +1,72 @@ +// -------------------------------------------------- +// external +// -------------------------------------------------- +use quote::ToTokens; + +// -------------------------------------------------- +// local +// -------------------------------------------------- +use crate::Ctxt; +use crate::symbol; + +#[derive(Debug)] +/// Represents struct attribute information +pub(crate) struct Container { + pub armtype: Option, +} +/// [`Container`] implementation +impl Container { + /// Extract out the `#[armtype(...)]` attributes from a container. + /// + /// Only container implemented is struct. + pub fn from_ast(cx: &Ctxt, item: &syn::DeriveInput) -> Self { + // -------------------------------------------------- + // init + // -------------------------------------------------- + let mut armtype = None; + + // -------------------------------------------------- + // loop through attrs + // -------------------------------------------------- + for attr in &item.attrs { + // -------------------------------------------------- + // only look for `armtype` + // -------------------------------------------------- + if attr.path() != symbol::ARMTYPE { + continue; + } + // -------------------------------------------------- + // parse out `armtype` attributes + // -------------------------------------------------- + match &attr.meta { + // -------------------------------------------------- + // handle all: `syn::MetaList` + // e.g. `armtype(<..>,),` + // -------------------------------------------------- + syn::Meta::List(list) => match symbol::Symbol::from(&list.path) { + + symbol::ARMTYPE => match armtype { + Some(_) => cx.error_spanned_by(&list, err!(DuplicateArmtypeEnumAttribute)), + None => armtype = syn::parse2(list.tokens.clone()) + .map_err(|err| cx.syn_error(err)) + .ok(), + }, + + _ => cx.error_spanned_by(&list.path, err!(UnknownContainerAttribute(list.path))), + }, + + _ => cx.error_spanned_by(&attr.meta.path(), err!(MalformedContainerAttribute)), + } + } + + // -------------------------------------------------- + // return + // -------------------------------------------------- + Container { + armtype: armtype.or_else(|| { + cx.error_spanned_by(&item.ident, err!(MissingArmtypeEnumAttribute)); + None + }) + } + } +} \ No newline at end of file diff --git a/impl/src/ast/mod.rs b/impl/src/ast/mod.rs new file mode 100644 index 0000000..d6548bd --- /dev/null +++ b/impl/src/ast/mod.rs @@ -0,0 +1,92 @@ +// -------------------------------------------------- +// mods +// -------------------------------------------------- +mod variant; +mod container; + +// -------------------------------------------------- +// re-exports +// -------------------------------------------------- +use container::*; + +// -------------------------------------------------- +// external +// -------------------------------------------------- +use syn::{punctuated::Punctuated, Token}; + +// -------------------------------------------------- +// local +// -------------------------------------------------- +use crate::Ctxt; + +/// A source data structure annotated with `#[derive(Const)]` parsed into an internal representation. +pub(crate) struct MainContainer<'a> { + /// The struct or enum name (without generics). + pub ident: syn::Ident, + /// Attributes on the structure, parsed for `thisenum`. + pub attrs: container::Container, + /// The contents of the enum + pub data: Vec>, + /// Any generics on the enum + pub generics: &'a syn::Generics, + /// Original input. + pub _original: &'a syn::DeriveInput, +} + +/// A variant of an enum. +pub(crate) struct MainVariant<'a> { + pub ident: syn::Ident, + pub attrs: variant::Variant, + pub original: &'a syn::Variant, +} + +/// [`MainContainer`] implementation +impl<'a> MainContainer<'a> { + /// Convert the raw [`syn`] ast into a parsed container object, collecting errors in `cx`. + pub fn from_ast( + cx: &Ctxt, + item: &'a syn::DeriveInput, + ) -> Option> { + + let attrs = Container::from_ast(cx, item); + + let data = match &item.data { + syn::Data::Enum(data) => Some(enum_from_ast(cx, &data.variants)), + + syn::Data::Struct(_) => { + cx.error_spanned_by(item, err!(UnsupportedContainer("struct"))); + return None; + } + + syn::Data::Union(_) => { + cx.error_spanned_by(item, err!(UnsupportedContainer("union"))); + return None; + } + }?; + + Some(MainContainer { + ident: item.ident.clone(), + attrs, + data, + generics: &item.generics, + _original: item, + }) + } +} + +#[inline(always)] +fn enum_from_ast<'a>( + cx: &Ctxt, + variants: &'a Punctuated, +) -> Vec> { + variants + .iter() + .map(|variant| { + MainVariant { + ident: variant.ident.clone(), + attrs: variant::Variant::from_ast(cx, variant), + original: variant, + } + }) + .collect() +} \ No newline at end of file diff --git a/impl/src/ast/variant.rs b/impl/src/ast/variant.rs new file mode 100644 index 0000000..57b2ec0 --- /dev/null +++ b/impl/src/ast/variant.rs @@ -0,0 +1,72 @@ +// -------------------------------------------------- +// external +// -------------------------------------------------- +use quote::ToTokens; + +// -------------------------------------------------- +// local +// -------------------------------------------------- +use crate::Ctxt; +use crate::symbol; + +#[derive(Debug)] +/// Represents variant attribute information +pub(crate) struct Variant { + pub value: Option, +} +/// [`Variant`] implementation +impl Variant { + /// Extract out the `#[value(...)]` attributes from an enum variant. + pub fn from_ast( + cx: &Ctxt, + variant: &syn::Variant, + ) -> Self { + let mut result = Self { value: None }; + // -------------------------------------------------- + // return if no attrs on variant + // -------------------------------------------------- + if variant.attrs.is_empty() { + return result; + } + + // -------------------------------------------------- + // loop through attrs + // -------------------------------------------------- + for attr in &variant.attrs { + // -------------------------------------------------- + // only look for `value` + // -------------------------------------------------- + if attr.path() != symbol::VALUE { + continue; + } + // -------------------------------------------------- + // parse out `value` attributes + // -------------------------------------------------- + match attr.meta { + // -------------------------------------------------- + // handle all: `syn::NameValue` + // e.g. `value = <..>,` + // -------------------------------------------------- + syn::Meta::NameValue(ref value) => match symbol::Symbol::from(&value.path) { + symbol::VALUE => match result.value { + Some(_) => cx.error_spanned_by(attr, err!(DuplicateValueVariantAttribute)), + None => result.value = Some(value.value.clone()), + }, + + _ => cx.error_spanned_by(attr, err!(UnknownVariantAttribute(value.path))), + } + + _ => cx.error_spanned_by(attr, err!(MalformedValueAttribute)), + } + } + + if result.value.is_none() { + cx.error_spanned_by(variant, err!(MissingValueVariantAttribute)); + } + + // -------------------------------------------------- + // return + // -------------------------------------------------- + result + } +} \ No newline at end of file diff --git a/impl/src/ctxt.rs b/impl/src/ctxt.rs new file mode 100644 index 0000000..5a4a6a5 --- /dev/null +++ b/impl/src/ctxt.rs @@ -0,0 +1,69 @@ +//! Taken from: [https://github.com/serde-rs/serde/blob/930401b0dd58a809fce34da091b8aa3d6083cb33/serde_derive/src/internals/ctxt.rs] + +use quote::ToTokens; +use std::cell::RefCell; +use std::fmt::Display; + +/// A type to collect errors together and format them. +/// +/// Dropping this object will cause a panic. It must be consumed using `check`. +/// +/// References can be shared since this type uses run-time exclusive mut checking. +#[derive(Default)] +pub(crate) struct Ctxt { + // The contents will be set to `None` during checking. This is so that checking can be + // enforced. + errors: RefCell>>, +} + +impl Ctxt { + /// Create a new context object. + /// + /// This object contains no errors, but will still trigger a panic if it is not `check`ed. + pub fn new() -> Self { + Ctxt { + errors: RefCell::new(Some(Vec::new())), + } + } + + /// Add an error to the context object with a tokenenizable object. + /// + /// The object is used for spanning in error messages. + pub fn error_spanned_by(&self, obj: A, msg: T) { + self.errors + .borrow_mut() + .as_mut() + .unwrap() + // Curb monomorphization from generating too many identical methods. + .push(syn::Error::new_spanned(obj.into_token_stream(), msg)); + } + + /// Add one of Syn's parse errors. + pub fn syn_error(&self, err: syn::Error) { + self.errors.borrow_mut().as_mut().unwrap().push(err); + } + + /// Consume this object, producing a formatted error string if there are errors. + pub fn check(self) -> syn::Result<()> { + let mut errors = self.errors.borrow_mut().take().unwrap().into_iter(); + + let mut combined = match errors.next() { + Some(first) => first, + None => return Ok(()), + }; + + for rest in errors { + combined.combine(rest); + } + + Err(combined) + } +} + +impl Drop for Ctxt { + fn drop(&mut self) { + if !std::thread::panicking() && self.errors.borrow().is_some() { + panic!("forgot to check for errors"); + } + } +} diff --git a/impl/src/err.rs b/impl/src/err.rs new file mode 100644 index 0000000..f0af971 --- /dev/null +++ b/impl/src/err.rs @@ -0,0 +1,49 @@ +//! Taken from `tinyklv` +#[doc(hidden)] +/// A quick way to add enum variants of [`crate::Error`] to the [`crate::Ctxt`] +/// error or [`syn::Error`] by transforming it into a [`str`]. +macro_rules! err { + // -------------------------------------------------- + // 1+ expr; 1+ literals + // -------------------------------------------------- + ($variant:ident($($expr:expr),* ; $($litstr:literal),*)) => { + $crate::Error::$variant( + $($expr.to_token_stream().to_string()),*, + $($litstr.to_string()),* + ).as_str() + }; + + // -------------------------------------------------- + // 1+ literals + // -------------------------------------------------- + ($variant:ident($($litstr:literal),*)) => { + $crate::Error::$variant( + $($litstr.to_string()),* + ).as_str() + }; + + // -------------------------------------------------- + // 1+ expressions + // -------------------------------------------------- + ($variant:ident($($expr:expr),*)) => { + $crate::Error::$variant( + $($expr.to_token_stream().to_string()),* + ).as_str() + }; + + // -------------------------------------------------- + // @String operator + // -------------------------------------------------- + ($variant:ident(@String $expr:expr)) => { + $crate::Error::$variant( + $expr + ).as_str() + }; + + // -------------------------------------------------- + // no arguments + // -------------------------------------------------- + ($variant:ident) => { + $crate::Error::$variant.as_str() + }; +} \ No newline at end of file diff --git a/impl/src/expand.rs b/impl/src/expand.rs new file mode 100644 index 0000000..49a5f66 --- /dev/null +++ b/impl/src/expand.rs @@ -0,0 +1,64 @@ +use quote::{quote, ToTokens}; +use proc_macro2::TokenStream; + +use crate::Ctxt; +use crate::ast::MainContainer; + +pub fn derive(input: &syn::DeriveInput) -> syn::Result { + // -------------------------------------------------- + // create a new error context + // -------------------------------------------------- + let cx = Ctxt::new(); + // -------------------------------------------------- + // get the parsed container for Const derive + // -------------------------------------------------- + let cont = match MainContainer::from_ast(&cx, input) { + Some(cont) => cont, + None => return Err(cx.check().unwrap_err()), + }; + // -------------------------------------------------- + // check for errors + // -------------------------------------------------- + cx.check()?; + + match &cont.data[0].attrs.value { + Some(x) => { + panic!("{}", x.to_token_stream()); + } + None => panic!("BRUH") + } + + // -------------------------------------------------- + // < crate level validation > + // -------------------------------------------------- + + // -------------------------------------------------- + // init + // -------------------------------------------------- + let mut expanded = quote! {}; + + // -------------------------------------------------- + // < impls > + // -------------------------------------------------- + // if let ( + // Some(key_enc), + // Some(len_enc), + // true, + // ) = ( + // cont.attrs.key.enc.as_ref(), + // cont.attrs.len.enc.as_ref(), + // all_encoders_exist, + // ) { + // let encode_impls = encode_impl::gen_encode_impl( + // &cont, + // &key_enc, + // &len_enc, + // ); + // expanded = quote! { + // #expanded + // #encode_impls + // } + // } + + Ok(expanded) +} \ No newline at end of file diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 37804e1..56301b0 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -1,650 +1,122 @@ #![doc = include_str!("../README.md")] +// -------------------------------------------------- +// mods +// -------------------------------------------------- +#[macro_use] +mod err; +mod ast; +mod ctxt; +mod expand; +mod symbol; +mod prelude; +// TODO: remove this once done +// mod lib_old; + // -------------------------------------------------- // external // -------------------------------------------------- -use quote::{ - quote, - ToTokens, -}; use syn::{ - Meta, - Data, - Type, - DataEnum, - Attribute, DeriveInput, - MetaNameValue, parse_macro_input, }; -use unzip_n::unzip_n; use thiserror::Error; -use proc_macro::TokenStream; // -------------------------------------------------- -// local +// re-exports // -------------------------------------------------- -mod prelude; -use prelude::*; -unzip_n!(3); +use crate::ctxt::Ctxt; -#[derive(Error, Debug)] -/// All errors that can occur while deriving [`Const`] -/// or [`ConstEach`] -enum Error { - #[error("`{0}` can only be derived for enums")] - DeriveForNonEnum(String), - #[error("Missing #[armtype = ...] attribute {0}, required for `{1}`-derived enum")] - MissingArmType(String, String), - #[error("Missing #[value = ...] attribute, expected for `{0}`-derived enum")] - MissingValue(String), - #[error("Attemping to parse non-literal attribute for `value`: not yet supported")] - NonLiteralValue, -} +// -------------------------------------------------- +// constants +// -------------------------------------------------- +const CRATE_NAME: &str = "thisenum"; +const DERIVE_NAME: &str = "Const"; #[proc_macro_derive(Const, attributes(value, armtype))] -/// Add's constants to each arm of an enum -/// -/// * To get the value as a reference, call the function [`::value`] -/// * However, direct comparison to non-reference values are possible with -/// [`PartialEq`] -/// -/// The `#[armtype = ...]` attribute is required for this macro to function, -/// and must be applied to **the enum**, since all values share the same type. -/// -/// All values set will return a [`&'static T`] reference. To the input type, -/// of [`T`] AND [`&T`]. If multiple references are used (e.g. `&&T`), then -/// the return type will be [`&'static &T`]. -/// -/// # Example -/// -/// ``` -/// use thisenum::Const; -/// -/// #[derive(Const, Debug)] -/// #[armtype(i32)] -/// enum MyEnum { -/// #[value = 0] -/// A, -/// #[value = 1] -/// B, -/// } -/// -/// #[derive(Const, Debug)] -/// #[armtype(&[u8])] -/// enum Tags { -/// #[value = b"\x00\x01\x7f"] -/// Key, -/// #[value = b"\xba\x5e"] -/// Length, -/// #[value = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"] -/// Data, -/// } -/// -/// fn main() { -/// // it's prefered to use the function call to `value` -/// // to get a [`&'static T`] reference to the value -/// assert_eq!(MyEnum::A.value(), &0); -/// assert_eq!(MyEnum::B.value(), &1); -/// assert_eq!(Tags::Key.value(), b"\x00\x01\x7f"); -/// assert_eq!(Tags::Length.value(), b"\xba\x5e"); -/// assert_eq!(Tags::Data.value(), b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"); -/// -/// // can also check equality without the function call. This must compare the input -/// // type defined in `#[armtype = ...]` -/// // -/// // to use this, use the `eq` feature in `Cargo.toml`: thisenum = { version = "x", features = ["eq"] } -/// #[cfg(feature = "eq")] -/// assert_eq!(Tags::Length, b"\xba\x5e"); -/// } -/// ``` -pub fn thisenum_const(input: TokenStream) -> TokenStream { - let name = "Const"; +/// [`thisenum`](crate) proc-macro to implement `Const` +pub fn const_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); + expand::derive(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[derive(Debug, Error)] +/// [`tinyklv`](crate) proc-macro errors +enum Error { + // -------------------------------------------------- - // extract the name, variants, and values - // -------------------------------------------------- - let enum_name = &input.ident; - let variants = match input.data { - Data::Enum(DataEnum { variants, .. }) => variants, - _ => panic!("{}", Error::DeriveForNonEnum(name.into())), - }; - // -------------------------------------------------- - // extract the type - // -------------------------------------------------- - let (type_name, deref) = match get_deref_type(&input.attrs) { - Some((type_name, deref)) => (type_name, deref), - None => panic!("{}", Error::MissingArmType("applied to enum".into(), name.into())), - }; - let type_name_raw = match get_type(&input.attrs) { - Some(type_name_raw) => type_name_raw, - None => panic!("{}", Error::MissingArmType("applied to enum".into(), name.into())), - }; - // -------------------------------------------------- - // get unique assigned values - // -------------------------------------------------- - let values = variants - .iter() - .map(|variant| get_val(name.into(), &variant.attrs)) - .collect::, _>>() - .unwrap(); - let values_string = values.iter().map(|v| v.to_string()).collect::>(); - let repeated_values_string = values_string.clone().into_iter().repeated(); - // -------------------------------------------------- - // generate the output tokens - // -------------------------------------------------- - let ( - // #[cfg(feature = "debug")] - _debug_arms, - variant_match_arms, - mut variant_inv_match_arms - ) = variants - .iter() - .map(|variant| { - let variant_name = &variant.ident; - // ------------------------------------------------ - // number of args in the variant - // ------------------------------------------------ - // e.g.: enum Test { VariantA(i23), VariantB(String, String) } - // will have 1 (i23) and 2 (String, String) - // ------------------------------------------------ - let num_args = match variant.fields { - syn::Fields::Named(syn::FieldsNamed { ref named, .. }) => named.len(), - syn::Fields::Unnamed(syn::FieldsUnnamed { ref unnamed, .. }) => unnamed.len(), - syn::Fields::Unit => 0, - }; - let value = match get_val(name.into(), &variant.attrs) { - Ok(value) => value, - Err(e) => panic!("{}", e), - }; - // ------------------------------------------------ - // check if the value is unique - // this is used to prevent unreachable arms - // ------------------------------------------------ - let val_repeated = repeated_values_string.contains(&value.to_string()); - // ------------------------------------------------ - // if the type input is a reference (e.g. &[u8] or &str) - // then the return type will be - // * `&'static [u8]` or - // * `&'static str` - // - // otherwise, if the input is not a reference (e.g. u8 or f32) - // then the return type will be - // * `&'static u8` or - // * `&'static f32` - // - // as a result, need to ensure we are removing / adding - // the `&` symbol wherever necessary - // ------------------------------------------------ - let args_tokens = match num_args { - 0 => quote! {}, - _ => { - let args = (0..num_args).map(|_| quote! { _ }); - quote! { ( #(#args),* ) } - }, - }; - // ------------------------------------------------ - // debug arms implementation - // ------------------------------------------------ - let debug_arm = match get_val(name.into(), &variant.attrs) { - Ok(_) => quote! { #enum_name::#variant_name #args_tokens => write!(f, concat!(stringify!(#enum_name), "::", stringify!(#variant_name), ": {:?}"), self.value()), }, - Err(e) => panic!("{}", e), - }; - // ------------------------------------------------ - // variant -> value - // ------------------------------------------------ - let vma = match deref { - true => quote! { #enum_name::#variant_name #args_tokens => #value, }, - false => quote! { #enum_name::#variant_name #args_tokens => &#value, }, - }; - // ------------------------------------------------ - // value -> variant - // ------------------------------------------------ - match (num_args, val_repeated) { - (0, false) => (debug_arm, vma, Some(quote! { #value => Ok(#enum_name::#variant_name), })), - (_, _) => (debug_arm, vma, None), - } - }) - .into_iter() - .unzip_n_vec(); - // -------------------------------------------------- - // get the vima for repeated values - // -------------------------------------------------- - let mut repeated_indices = values_string - .clone() - .into_iter() - .repeated_idx(); - repeated_indices.sort_by(|a, b| b.cmp(a)); - repeated_indices - .iter() - .for_each(|i| { variant_inv_match_arms.remove(*i); } ); - let variant_inv_match_arms_repeated = values_string - .clone() - .into_iter() - .positions() - .iter() - .map(|(_, pos)| match pos.len() { - ..=1 => quote! {}, - _ => { - let val = values[pos[0]].clone(); - quote! { #val => Err(::thisenum::Error::UnreachableValue(format!("{:?}", #val))), } - } - }) - .collect::>(); - // -------------------------------------------------- - // get all the indices of variants which have nested args + // container parsing // -------------------------------------------------- - let arg_indices = variant_inv_match_arms - .iter() - .enumerate() - .filter(|(i, v)| v.is_none() && !repeated_indices.contains(&i)) - .map(|(i, _)| i) - .collect::>(); - let variant_inv_match_arms_args = values - .clone() - .into_iter() - .zip(variants) - .enumerate() - .filter(|(i, _)| arg_indices.contains(i)) - .map(|(_, (value, variant))| { - let variant_name = &variant.ident; - quote! { #value => Err(::thisenum::Error::UnableToReturnVariant(stringify!(#variant_name).into())), } - }) - .collect::>(); + + #[error("\ + {c} does not support #[derive({d})] for {0}s.", + c = CRATE_NAME, + d = DERIVE_NAME, + )] + UnsupportedContainer(String), + // -------------------------------------------------- - // see deref comment above + // container (only enum) attributes // -------------------------------------------------- - let into_impl = match deref { - false => quote! { - #[automatically_derived] - #[doc = concat!(" [`Into`] implementation for [`", stringify!(#enum_name), "`]")] - impl ::std::convert::Into<#type_name_raw> for #enum_name { - #[inline] - fn into(self) -> #type_name_raw { - *self.value() - } - } - }, - true => quote! { }, - }; - let mut expanded = quote! { - #[automatically_derived] - impl #enum_name { - #[inline] - /// Returns the value of the enum variant - /// defined by [`Const`] - /// - /// # Returns - /// - #[doc = concat!(" * [`&'static ", stringify!(#type_name), "`]")] - pub fn value(&self) -> &'static #type_name { - match self { - #( #variant_match_arms )* - } - } - } - #into_impl - }; - if cfg!(feature = "debug") { - expanded = quote! { - #expanded - #[automatically_derived] - #[doc = concat!(" [`Debug`] implementation for [`", stringify!(#enum_name), "`]")] - impl ::std::fmt::Debug for #enum_name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match self { - #( #_debug_arms )* - } - } - } - }; - } + #[error("\ + Unknown enum attribute: `{0}`. +Expected {s}.", + s = symbol::CONT_SYMBOLS, + )] + UnknownContainerAttribute(String), - if cfg!(feature = "eq") { - let variant_par_eq_lhs = match deref { - true => quote! { &self.value() == other }, - false => quote! { self.value() == other }, - }; - let variant_par_eq_rhs = match deref { - true => quote! { &other.value() == self }, - false => quote! { other.value() == self }, - }; - expanded = quote! { - #expanded - #[automatically_derived] - #[doc = concat!(" [`PartialEq<", stringify!(#type_name_raw) ,">`] implementation for [`", stringify!(#enum_name), "`]")] - /// - #[doc = concat!(" This is the LHS of the [`PartialEq`] implementation between [`", stringify!(#enum_name), "`] and [`", stringify!(#type_name_raw), "`]")] - /// - /// # Returns - /// - /// * [`true`] if the type and the enum are equal - /// * [`false`] if the type and the enum are not equal - impl ::std::cmp::PartialEq<#type_name_raw> for #enum_name { - #[inline] - fn eq(&self, other: &#type_name_raw) -> bool { - #variant_par_eq_lhs - } - } - #[automatically_derived] - #[doc = concat!(" [`PartialEq<", stringify!(#enum_name) ,">`] implementation for [`", stringify!(#type_name_raw), "`]")] - /// - #[doc = concat!(" This is the RHS of the [`PartialEq`] implementation between [`", stringify!(#enum_name), "`] and [`", stringify!(#type_name_raw), "`]")] - /// - /// # Returns - /// - /// * [`true`] if the enum and the type are equal - /// * [`false`] if the enum and the type are not equal - impl ::std::cmp::PartialEq<#enum_name> for #type_name_raw { - #[inline] - fn eq(&self, other: &#enum_name) -> bool { - #variant_par_eq_rhs - } - } - }; - } + #[error("\ + Malformed container attribute, expected: `#[{s}()]`.", + s = symbol::ARMTYPE, + )] + MalformedContainerAttribute, - let variant_inv_match_arms = variant_inv_match_arms.into_iter().filter(|v| v.is_some()).map(|v| v.unwrap()); - expanded = quote! { - #expanded - #[automatically_derived] - #[doc = concat!(" [`TryFrom`] implementation for [`", stringify!(#enum_name), "`]")] - /// - /// This is able to be derived since none of the Arms of the Enum had - /// any arguments. If that is the case, this implementation is - /// non-existent. - /// - /// # Returns - /// - /// * [`Ok(T)`] where `T` is the enum variant - /// * [`Err(Error)`] if the conversion fails - impl ::std::convert::TryFrom<#type_name_raw> for #enum_name { - type Error = ::thisenum::Error; - #[inline] - fn try_from(value: #type_name_raw) -> Result { - match value { - #( #variant_inv_match_arms )* - #( #variant_inv_match_arms_repeated )* - #( #variant_inv_match_arms_args )* - _ => Err(::thisenum::Error::InvalidValue(format!("{:?}", value), stringify!(#enum_name).into())), - } - } - } - }; - // -------------------------------------------------- - // return - // -------------------------------------------------- - TokenStream::from(expanded) -} + #[error("\ + Missing required `{s}` enum attribute to describe return type of the value's.", + s = symbol::ARMTYPE, + )] + MissingArmtypeEnumAttribute, + + #[error("\ + Duplicate `{s}` enum attribute.", + s = symbol::ARMTYPE, + )] + DuplicateArmtypeEnumAttribute, -#[proc_macro_derive(ConstEach, attributes(value, armtype))] -/// Add's constants of any type to each arm of an enum -/// -/// To get the value, the type must be explicitly passed -/// as a generic to [`::value`]. This will automatically -/// try to convert constant to the expected type using [`std::any::Any`] -/// and [`downcast_ref`]. Currently [`TryFrom`] is not supported, so typing -/// is fairly strict. Upon failure, it will return [`None`]. -/// -/// * To get the value as a reference, call the function [`::value`] -/// * Unlike [`Const`], this macro does not enable direct comparison -/// using [`PartialEq`] when imported using the `eq` feature. -/// -/// The `#[armtype = ...]` attribute is **NOT*** required for this macro to function, -/// but ***CAN** be applied to ***each individual arm*** of the enum, since values -/// are not expected to share a type. If no type is given, then the type is -/// inferred from the literal value in the `#[value = ...]` attribute. -/// -/// All values set will return a [`Option<&'static T>`] reference. To the input type, -/// of [`T`] AND [`&T`]. If multiple references are used (e.g. `&&T`), then -/// the return type will be [`Option<&'static &T>`]. -/// -/// # Example -/// -/// ``` -/// use thisenum::ConstEach; -/// -/// #[derive(ConstEach, Debug)] -/// enum MyEnum { -/// #[armtype(u8)] -/// #[value = 0xAA] -/// A, -/// #[value = "test3"] -/// B, -/// } -/// -/// #[derive(ConstEach, Debug)] -/// enum Tags { -/// #[value = b"\x00\x01"] -/// Key, -/// #[armtype(u16)] -/// #[value = 24250] -/// Length, -/// #[armtype(&[u8])] -/// #[value = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"] -/// Data, -/// } -/// -/// fn main() { -/// // [`ConstEach`] examples -/// assert!(MyEnum::A.value::().is_some()); -/// assert!(MyEnum::A.value::>().is_none()); -/// assert!(MyEnum::B.value::().is_none()); -/// assert!(MyEnum::B.value::<&str>().is_some()); -/// assert!(Tags::Data.value::<&[u8]>().is_some()); -/// -/// // An infered type. This will be as strict as possible, -/// // therefore [`&[u8]`] will fail but [`&[u8; 2]`] will succeed -/// assert!(Tags::Key.value::<&[u8; 2]>().is_some()); -/// assert!(Tags::Key.value::<&[u8; 5]>().is_none()); -/// assert!(Tags::Key.value::<&[u8]>().is_none()); -/// assert!(u16::from_le_bytes(**Tags::Key.value::<&[u8; 2]>().unwrap()) == 0x0100); -/// -/// // casting as anything other than the defined / inferred type will -/// // fail, since this uses [`downcast_ref`] from [`std::any::Any`] -/// assert!(Tags::Length.value::().is_some()); -/// assert!(Tags::Length.value::().is_none()); -/// assert!(Tags::Length.value::().is_none()); -/// -/// // however, can always convert to a different type -/// // after value is successfully acquired -/// assert!(*Tags::Length.value::().unwrap() as u32 == 24250); -/// } -/// ``` -pub fn thisenum_const_each(input: TokenStream) -> TokenStream { - let name = "ConstEach"; - let input = parse_macro_input!(input as DeriveInput); - // -------------------------------------------------- - // extract the name, variants, and values - // -------------------------------------------------- - let enum_name = &input.ident; - let variants = match input.data { - Data::Enum(DataEnum { variants, .. }) => variants, - _ => panic!("{}", Error::DeriveForNonEnum(name.into())), - }; // -------------------------------------------------- - // generate the output tokens + // variant attribute (only value is used) // -------------------------------------------------- - let variant_code = variants.iter().map(|variant| { - let variant_name = &variant.ident; - match (get_type(&variant.attrs), get_val(name.into(), &variant.attrs)) { - // ------------------------------------------------ - // if type is specified, use it - // ------------------------------------------------ - (Some(typ), Ok(value)) => quote! { - #enum_name::#variant_name => { - let val: &dyn ::std::any::Any = &(#value as #typ); - val.downcast_ref::() - }, - }, - // ------------------------------------------------ - // no type specified, try to infer - // ------------------------------------------------ - (None, Ok(value)) => quote! { - #enum_name::#variant_name => { - let val: &dyn ::std::any::Any = &#value; - val.downcast_ref::() - }, - }, - // ------------------------------------------------ - // unable to infer type - // ------------------------------------------------ - (_, Err(_)) => quote! { #enum_name::#variant_name => None, }, - } - }); - // ------------------------------------------------ - // return - // ------------------------------------------------ - let expanded = quote! { - #[automatically_derived] - #[doc = concat!(" [`ConstEach`] implementation for [`", stringify!(#enum_name), "`]")] - impl #enum_name { - pub fn value(&self) -> Option<&'static T> { - match self { - #( #variant_code )* - _ => None, - } - } - } - }; - TokenStream::from(expanded) -} + #[error("\ + Unknown variant attribute: `{0}`. +Expected {s}.", + s = symbol::VARIANT_SYMBOLS + )] + UnknownVariantAttribute(String), -/// Helper function to extract the value from a [`MetaNameValue`], aka `#[value = ]` -/// -/// # Input -/// -/// ```text -/// #[value = ] -/// ``` -/// -/// # Output -/// -/// [`TokenStream`] containing the value ``, or [`Err`] if the attribute is not present / invalid -fn get_val(name: String, attrs: &[Attribute]) -> Result { - for attr in attrs { - if !attr.path.is_ident("value") { continue; } - match attr.parse_meta() { - Ok(meta) => match meta { - Meta::NameValue(MetaNameValue { lit, .. }) => return Ok(lit.into_token_stream()), - Meta::List(list) => { - let tokens = list.nested.iter().map(|nested_meta| { - match nested_meta { - syn::NestedMeta::Lit(lit) => lit.to_token_stream(), - syn::NestedMeta::Meta(meta) => meta.to_token_stream(), - } - }); - return Ok(quote! { #( #tokens )* }); - } - Meta::Path(_) => return Ok(meta.into_token_stream()) - }, - Err(_) => { - return Err(Error::NonLiteralValue); - /* - // Maybe for future: - // -------------------------------------------------- - let elems = attr - .to_token_stream() - .to_string(); - // println!("elems: {}", elems); - let mut elems = elems - .trim() - .trim_start_matches("#[") - .rsplit_once("]") - .unwrap() - .0 - .split("=") - .collect::>(); - // println!("elems: {:?}", elems); - elems.remove(0); - // println!("elems: {:?}", elems); - return Ok(elems - .join("=") - .trim() - .parse::()?); - // -------------------------------------------------- - */ - }, - } - } - Err(Error::MissingValue(name)) -} + #[error("\ + Malformed variant attribute, expected name-value attribute: `#[{s} = ]`.", + s = symbol::VALUE, + )] + MalformedValueAttribute, -/// Helper function to extract the type from the [`Attribute`], aka `#[armtype()]` -/// -/// Will indicate whether or not the type should be dereferenced or not. Useful -/// for the [`Const`] macro -/// -/// # Input -/// -/// ```text -/// #[armtype()] -/// ``` -/// -/// # Output -/// -/// [`None`] if the attribute is not present / invalid -/// -/// Otherwise a tuple: -/// -/// * 0 - [`Type`] containing the type `` (already de-referenced) -/// * 1 - An additional flag that indicates if the type has been de-referenced -fn get_deref_type(attrs: &[Attribute]) -> Option<(Type, bool)> { - for attr in attrs { - if !attr.path.is_ident("armtype") { continue; } - let tokens = match attr.parse_args::() { - Ok(tokens) => tokens, - Err(_) => return None, - }; - let deref = tokens - .to_string() - .trim() - .starts_with('&'); - let tokens = match deref { - true => { - let mut tokens = tokens.into_iter(); - let _ = tokens.next(); - tokens.collect::() - } - false => tokens, - }; - return match syn::parse2::(tokens).ok() { - Some(type_name) => Some((type_name, deref)), - None => None - } - } - None -} + #[error("\ + Missing required `{s}` variant attribute.", + s = symbol::VALUE, + )] + MissingValueVariantAttribute, -/// Helper function to extract the type from the [`Attribute`], aka `#[armtype()]` -/// -/// Will return the raw [`Type`]. Useful for the [`Const`] and the [`ConstEach`] -/// macros -/// -/// # Input -/// -/// ```text -/// #[armtype()] -/// ``` -/// -/// # Output -/// -/// [`None`] if the attribute is not present / invalid -/// -/// Otherwise [`Some`] containing the type `` -fn get_type(attrs: &[Attribute]) -> Option { - for attr in attrs { - if !attr.path.is_ident("armtype") { continue; } - let tokens = match attr.parse_args::() { - Ok(tokens) => tokens, - Err(_) => return None, - }; - return syn::parse2::( - tokens - .into_iter() - .collect::() - ).ok() + #[error("\ + Duplicate `{s}` variant attribute.", + s = symbol::VALUE, + )] + DuplicateValueVariantAttribute, +} +/// [`Error`] implementation +impl Error { + fn as_str(&self) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Owned(self.to_string()) } - None } \ No newline at end of file diff --git a/impl/src/lib_old.rs b/impl/src/lib_old.rs new file mode 100644 index 0000000..c83d9a0 --- /dev/null +++ b/impl/src/lib_old.rs @@ -0,0 +1,640 @@ +#![doc = include_str!("../README.md")] +// -------------------------------------------------- +// external +// -------------------------------------------------- +use quote::{ + quote, + ToTokens, +}; +use syn::{ + Meta, + Data, + Type, + DataEnum, + Attribute, + DeriveInput, + MetaNameValue, + parse_macro_input, +}; +use unzip_n::unzip_n; +use thiserror::Error; +use proc_macro::TokenStream; + +// -------------------------------------------------- +// local +// -------------------------------------------------- +use super::prelude::*; +unzip_n!(3); + +#[derive(Error, Debug)] +/// All errors that can occur while deriving [`Const`] +/// or [`ConstEach`] +enum Error { + #[error("`{0}` can only be derived for enums")] + DeriveForNonEnum(String), + #[error("Missing #[armtype = ...] attribute {0}, required for `{1}`-derived enum")] + MissingArmType(String, String), + #[error("Missing #[value = ...] attribute, expected for `{0}`-derived enum")] + MissingValue(String), + #[error("Attemping to parse non-literal attribute for `value`: not yet supported")] + NonLiteralValue, +} + +// #[proc_macro_derive(Const, attributes(value, armtype))] +/// Add's constants to each arm of an enum +/// +/// * To get the value as a reference, call the function [`::value`] +/// * However, direct comparison to non-reference values are possible with +/// [`PartialEq`] +/// +/// The `#[armtype = ...]` attribute is required for this macro to function, +/// and must be applied to **the enum**, since all values share the same type. +/// +/// All values set will return a [`&'static T`] reference. To the input type, +/// of [`T`] AND [`&T`]. If multiple references are used (e.g. `&&T`), then +/// the return type will be [`&'static &T`]. +/// +/// # Example +/// +/// ``` +/// use thisenum::Const; +/// +/// #[derive(Const, Debug)] +/// #[armtype(i32)] +/// enum MyEnum { +/// #[value = 0] +/// A, +/// #[value = 1] +/// B, +/// } +/// +/// #[derive(Const, Debug)] +/// #[armtype(&[u8])] +/// enum Tags { +/// #[value = b"\x00\x01\x7f"] +/// Key, +/// #[value = b"\xba\x5e"] +/// Length, +/// #[value = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"] +/// Data, +/// } +/// +/// fn main() { +/// // it's prefered to use the function call to `value` +/// // to get a [`&'static T`] reference to the value +/// assert_eq!(MyEnum::A.value(), &0); +/// assert_eq!(MyEnum::B.value(), &1); +/// assert_eq!(Tags::Key.value(), b"\x00\x01\x7f"); +/// assert_eq!(Tags::Length.value(), b"\xba\x5e"); +/// assert_eq!(Tags::Data.value(), b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"); +/// +/// // can also check equality without the function call. This must compare the input +/// // type defined in `#[armtype = ...]` +/// // +/// // to use this, use the `eq` feature in `Cargo.toml`: thisenum = { version = "x", features = ["eq"] } +/// #[cfg(feature = "eq")] +/// assert_eq!(Tags::Length, b"\xba\x5e"); +/// } +/// ``` +pub fn thisenum_const(input: TokenStream) -> TokenStream { + const NAME: &str = "Const"; + let input = parse_macro_input!(input as DeriveInput); + // -------------------------------------------------- + // extract the name, variants, and values + // -------------------------------------------------- + let enum_name = &input.ident; + let enum_generics = input.generics.clone(); + println!("enum name = {:#?}", input.generics.to_token_stream().to_string()); + let variants = match input.data { + Data::Enum(DataEnum { variants, .. }) => variants, + _ => panic!("{}", Error::DeriveForNonEnum(NAME.into())), + }; + // -------------------------------------------------- + // extract the type + // -------------------------------------------------- + let (type_name, deref) = match get_deref_type(&input.attrs) { + Some((type_name, deref)) => (type_name, deref), + None => panic!("{}", Error::MissingArmType("applied to enum".into(), NAME.into())), + }; + let type_name_raw = match get_type(&input.attrs) { + Some(type_name_raw) => type_name_raw, + None => panic!("{}", Error::MissingArmType("applied to enum".into(), NAME.into())), + }; + // -------------------------------------------------- + // get unique assigned values + // -------------------------------------------------- + let values = variants + .iter() + .map(|variant| get_val(NAME.into(), &variant.attrs)) + .collect::, _>>() + .unwrap(); + let values_string = values.iter().map(|v| v.to_string()).collect::>(); + let repeated_values_string = values_string.clone().into_iter().repeated(); + // -------------------------------------------------- + // generate the output tokens + // -------------------------------------------------- + let ( + debug_arms, + variant_match_arms, + mut variant_inv_match_arms + ) = variants + .iter() + .map(|variant| { + let variant_name = &variant.ident; + // ------------------------------------------------ + // number of args in the variant + // ------------------------------------------------ + // e.g.: enum Test { VariantA(i23), VariantB(String, String) } + // will have 1 (i23) and 2 (String, String) + // ------------------------------------------------ + let num_args = match variant.fields { + syn::Fields::Named(syn::FieldsNamed { ref named, .. }) => named.len(), + syn::Fields::Unnamed(syn::FieldsUnnamed { ref unnamed, .. }) => unnamed.len(), + syn::Fields::Unit => 0, + }; + let value = match get_val(NAME.into(), &variant.attrs) { + Ok(value) => value, + Err(e) => panic!("{}", e), + }; + // ------------------------------------------------ + // check if the value is unique + // this is used to prevent unreachable arms + // ------------------------------------------------ + let val_repeated = repeated_values_string.contains(&value.to_string()); + // ------------------------------------------------ + // if the type input is a reference (e.g. &[u8] or &str) + // then the return type will be + // * `&'static [u8]` or + // * `&'static str` + // + // otherwise, if the input is not a reference (e.g. u8 or f32) + // then the return type will be + // * `&'static u8` or + // * `&'static f32` + // + // as a result, need to ensure we are removing / adding + // the `&` symbol wherever necessary + // ------------------------------------------------ + let args_tokens = match num_args { + 0 => quote! {}, + _ => { + let args = (0..num_args).map(|_| quote! { _ }); + quote! { ( #(#args),* ) } + }, + }; + // ------------------------------------------------ + // debug arms implementation + // ------------------------------------------------ + let debug_arm = match get_val(NAME.into(), &variant.attrs) { + Ok(_) => quote! { #enum_name::#variant_name #args_tokens => write!(f, concat!(stringify!(#enum_name), "::", stringify!(#variant_name), ": {:?}"), self.value()), }, + Err(e) => panic!("{}", e), + }; + // ------------------------------------------------ + // variant -> value + // ------------------------------------------------ + let vma = match deref { + true => quote! { #enum_name::#variant_name #args_tokens => #value, }, + false => quote! { #enum_name::#variant_name #args_tokens => &#value, }, + }; + // ------------------------------------------------ + // value -> variant + // ------------------------------------------------ + match (num_args, val_repeated) { + (0, false) => (debug_arm, vma, Some(quote! { #value => Ok(#enum_name::#variant_name), })), + (_, _) => (debug_arm, vma, None), + } + }) + .into_iter() + .unzip_n_vec(); + // -------------------------------------------------- + // get the vima for repeated values + // -------------------------------------------------- + let mut repeated_indices = values_string + .clone() + .into_iter() + .repeated_idx(); + repeated_indices.sort_by(|a, b| b.cmp(a)); + repeated_indices + .iter() + .for_each(|i| { variant_inv_match_arms.remove(*i); } ); + let variant_inv_match_arms_repeated = values_string + .clone() + .into_iter() + .positions() + .iter() + .map(|(_, pos)| match pos.len() { + ..=1 => quote! {}, + _ => { + let val = values[pos[0]].clone(); + quote! { #val => Err(::thisenum::Error::UnreachableValue(format!("{:?}", #val))), } + } + }) + .collect::>(); + // -------------------------------------------------- + // get all the indices of variants which have nested args + // -------------------------------------------------- + let arg_indices = variant_inv_match_arms + .iter() + .enumerate() + .filter(|(i, v)| v.is_none() && !repeated_indices.contains(&i)) + .map(|(i, _)| i) + .collect::>(); + let variant_inv_match_arms_args = values + .clone() + .into_iter() + .zip(variants) + .enumerate() + .filter(|(i, _)| arg_indices.contains(i)) + .map(|(_, (value, variant))| { + let variant_name = &variant.ident; + quote! { #value => Err(::thisenum::Error::UnableToReturnVariant(stringify!(#variant_name).into())), } + }) + .collect::>(); + // -------------------------------------------------- + // see deref comment above + // -------------------------------------------------- + let variant_par_eq_lhs = match deref { + true => quote! { &self.value() == other }, + false => quote! { self.value() == other }, + }; + let variant_par_eq_rhs = match deref { + true => quote! { &other.value() == self }, + false => quote! { other.value() == self }, + }; + let into_impl = match deref { + false => quote! { + #[automatically_derived] + #[doc = concat!(" [`Into`] implementation for [`", stringify!(#enum_name), "`]")] + impl ::std::convert::Into<#type_name_raw> for #enum_name { + #[inline] + fn into(self) -> #type_name_raw { + *self.value() + } + } + }, + true => quote! { }, + }; + // -------------------------------------------------- + // return + // -------------------------------------------------- + let mut expanded = quote! { + #[automatically_derived] + impl #enum_name { + #[inline] + /// Returns the value of the enum variant + /// defined by [`Const`] + /// + /// # Returns + /// + #[doc = concat!(" * [`&'static ", stringify!(#type_name), "`]")] + pub const fn value(&self) -> &'static #type_name { + match self { + #( #variant_match_arms )* + } + } + } + #[automatically_derived] + #[cfg(feature = "eq")] + #[doc = concat!(" [`PartialEq<", stringify!(#type_name_raw) ,">`] implementation for [`", stringify!(#enum_name), "`]")] + /// + #[doc = concat!(" This is the LHS of the [`PartialEq`] implementation between [`", stringify!(#enum_name), "`] and [`", stringify!(#type_name_raw), "`]")] + /// + /// # Returns + /// + /// * [`true`] if the type and the enum are equal + /// * [`false`] if the type and the enum are not equal + impl ::std::cmp::PartialEq<#type_name_raw> for #enum_name { + #[inline] + fn eq(&self, other: &#type_name_raw) -> bool { + #variant_par_eq_lhs + } + } + #[automatically_derived] + #[cfg(feature = "eq")] + #[doc = concat!(" [`PartialEq<", stringify!(#enum_name) ,">`] implementation for [`", stringify!(#type_name_raw), "`]")] + /// + #[doc = concat!(" This is the RHS of the [`PartialEq`] implementation between [`", stringify!(#enum_name), "`] and [`", stringify!(#type_name_raw), "`]")] + /// + /// # Returns + /// + /// * [`true`] if the enum and the type are equal + /// * [`false`] if the enum and the type are not equal + impl ::std::cmp::PartialEq<#enum_name> for #type_name_raw { + #[inline] + fn eq(&self, other: &#enum_name) -> bool { + #variant_par_eq_rhs + } + } + #[automatically_derived] + #[doc = concat!(" [`Debug`] implementation for [`", stringify!(#enum_name), "`]")] + impl ::std::fmt::Debug for #enum_name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #( #debug_arms )* + } + } + } + #into_impl + }; + let variant_inv_match_arms = variant_inv_match_arms.into_iter().filter(|v| v.is_some()).map(|v| v.unwrap()); + expanded = quote! { + #expanded + #[automatically_derived] + #[doc = concat!(" [`TryFrom`] implementation for [`", stringify!(#enum_name), "`]")] + /// + /// This is able to be derived since none of the Arms of the Enum had + /// any arguments. If that is the case, this implementation is + /// non-existent. + /// + /// # Returns + /// + /// * [`Ok(T)`] where `T` is the enum variant + /// * [`Err(Error)`] if the conversion fails + impl ::std::convert::TryFrom<#type_name_raw> for #enum_name { + type Error = ::thisenum::Error; + #[inline] + fn try_from(value: #type_name_raw) -> Result { + match value { + #( #variant_inv_match_arms )* + #( #variant_inv_match_arms_repeated )* + #( #variant_inv_match_arms_args )* + _ => Err(::thisenum::Error::InvalidValue(format!("{:?}", value), stringify!(#enum_name).into())), + } + } + } + }; + TokenStream::from(expanded) +} + +#[deprecated] +// #[proc_macro_derive(ConstEach, attributes(value, armtype))] +/// Add's constants of any type to each arm of an enum +/// +/// To get the value, the type must be explicitly passed +/// as a generic to [`::value`]. This will automatically +/// try to convert constant to the expected type using [`std::any::Any`] +/// and [`downcast_ref`]. Currently [`TryFrom`] is not supported, so typing +/// is fairly strict. Upon failure, it will return [`None`]. +/// +/// * To get the value as a reference, call the function [`::value`] +/// * Unlike [`Const`], this macro does not enable direct comparison +/// using [`PartialEq`] when imported using the `eq` feature. +/// +/// The `#[armtype = ...]` attribute is **NOT*** required for this macro to function, +/// but ***CAN** be applied to ***each individual arm*** of the enum, since values +/// are not expected to share a type. If no type is given, then the type is +/// inferred from the literal value in the `#[value = ...]` attribute. +/// +/// All values set will return a [`Option<&'static T>`] reference. To the input type, +/// of [`T`] AND [`&T`]. If multiple references are used (e.g. `&&T`), then +/// the return type will be [`Option<&'static &T>`]. +/// +/// # Example +/// +/// ``` +/// use thisenum::ConstEach; +/// +/// #[derive(ConstEach, Debug)] +/// enum MyEnum { +/// #[armtype(u8)] +/// #[value = 0xAA] +/// A, +/// #[value = "test3"] +/// B, +/// } +/// +/// #[derive(ConstEach, Debug)] +/// enum Tags { +/// #[value = b"\x00\x01"] +/// Key, +/// #[armtype(u16)] +/// #[value = 24250] +/// Length, +/// #[armtype(&[u8])] +/// #[value = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"] +/// Data, +/// } +/// +/// fn main() { +/// // [`ConstEach`] examples +/// assert!(MyEnum::A.value::().is_some()); +/// assert!(MyEnum::A.value::>().is_none()); +/// assert!(MyEnum::B.value::().is_none()); +/// assert!(MyEnum::B.value::<&str>().is_some()); +/// assert!(Tags::Data.value::<&[u8]>().is_some()); +/// +/// // An infered type. This will be as strict as possible, +/// // therefore [`&[u8]`] will fail but [`&[u8; 2]`] will succeed +/// assert!(Tags::Key.value::<&[u8; 2]>().is_some()); +/// assert!(Tags::Key.value::<&[u8; 5]>().is_none()); +/// assert!(Tags::Key.value::<&[u8]>().is_none()); +/// assert!(u16::from_le_bytes(**Tags::Key.value::<&[u8; 2]>().unwrap()) == 0x0100); +/// +/// // casting as anything other than the defined / inferred type will +/// // fail, since this uses [`downcast_ref`] from [`std::any::Any`] +/// assert!(Tags::Length.value::().is_some()); +/// assert!(Tags::Length.value::().is_none()); +/// assert!(Tags::Length.value::().is_none()); +/// +/// // however, can always convert to a different type +/// // after value is successfully acquired +/// assert!(*Tags::Length.value::().unwrap() as u32 == 24250); +/// } +/// ``` +pub fn thisenum_const_each(input: TokenStream) -> TokenStream { + const NAME: &str = "ConstEach"; + let input = parse_macro_input!(input as DeriveInput); + // -------------------------------------------------- + // extract the name, variants, and values + // -------------------------------------------------- + let enum_name = &input.ident; + let variants = match input.data { + Data::Enum(DataEnum { variants, .. }) => variants, + _ => panic!("{}", Error::DeriveForNonEnum(NAME.into())), + }; + // -------------------------------------------------- + // generate the output tokens + // -------------------------------------------------- + let variant_code = variants.iter().map(|variant| { + let variant_name = &variant.ident; + match (get_type(&variant.attrs), get_val(NAME.into(), &variant.attrs)) { + // ------------------------------------------------ + // if type is specified, use it + // ------------------------------------------------ + (Some(typ), Ok(value)) => quote! { + #enum_name::#variant_name => { + let val: &dyn ::std::any::Any = &(#value as #typ); + val.downcast_ref::() + }, + + }, + // ------------------------------------------------ + // no type specified, try to infer + // ------------------------------------------------ + (None, Ok(value)) => quote! { + #enum_name::#variant_name => { + let val: &dyn ::std::any::Any = &#value; + val.downcast_ref::() + }, + }, + // ------------------------------------------------ + // unable to infer type + // ------------------------------------------------ + (_, Err(_)) => quote! { #enum_name::#variant_name => None, }, + } + }); + // ------------------------------------------------ + // return + // ------------------------------------------------ + let expanded = quote! { + #[automatically_derived] + #[doc = concat!(" [`ConstEach`] implementation for [`", stringify!(#enum_name), "`]")] + impl #enum_name { + pub fn value(&self) -> Option<&'static T> { + match self { + #( #variant_code )* + _ => None, + } + } + } + }; + TokenStream::from(expanded) +} + +/// Helper function to extract the value from a [`MetaNameValue`], aka `#[value = ]` +/// +/// # Input +/// +/// ```text +/// #[value = ] +/// ``` +/// +/// # Output +/// +/// [`TokenStream`] containing the value ``, or [`Err`] if the attribute is not present / invalid +fn get_val(name: String, attrs: &[Attribute]) -> Result { + for attr in attrs { + if !attr.path.is_ident("value") { continue; } + match attr.parse_meta() { + Ok(meta) => match meta { + Meta::NameValue(MetaNameValue { lit, .. }) => return Ok(lit.into_token_stream()), + Meta::List(list) => { + let tokens = list.nested.iter().map(|nested_meta| { + match nested_meta { + syn::NestedMeta::Lit(lit) => lit.to_token_stream(), + syn::NestedMeta::Meta(meta) => meta.to_token_stream(), + } + }); + return Ok(quote! { #( #tokens )* }); + } + Meta::Path(_) => return Ok(meta.into_token_stream()) + }, + Err(_) => { + return Err(Error::NonLiteralValue); + /* + // Maybe for future: + // -------------------------------------------------- + let elems = attr + .to_token_stream() + .to_string(); + // println!("elems: {}", elems); + let mut elems = elems + .trim() + .trim_start_matches("#[") + .rsplit_once("]") + .unwrap() + .0 + .split("=") + .collect::>(); + // println!("elems: {:?}", elems); + elems.remove(0); + // println!("elems: {:?}", elems); + return Ok(elems + .join("=") + .trim() + .parse::()?); + // -------------------------------------------------- + */ + }, + } + } + Err(Error::MissingValue(name)) +} + +/// Helper function to extract the type from the [`Attribute`], aka `#[armtype()]` +/// +/// Will indicate whether or not the type should be dereferenced or not. Useful +/// for the [`Const`] macro +/// +/// # Input +/// +/// ```text +/// #[armtype()] +/// ``` +/// +/// # Output +/// +/// [`None`] if the attribute is not present / invalid +/// +/// Otherwise a tuple: +/// +/// * 0 - [`Type`] containing the type `` (already de-referenced) +/// * 1 - An additional flag that indicates if the type has been de-referenced +fn get_deref_type(attrs: &[Attribute]) -> Option<(Type, bool)> { + for attr in attrs { + if !attr.path.is_ident("armtype") { continue; } + let tokens = match attr.parse_args::() { + Ok(tokens) => tokens, + Err(_) => return None, + }; + let deref = tokens + .to_string() + .trim() + .starts_with('&'); + let tokens = match deref { + true => { + let mut tokens = tokens.into_iter(); + let _ = tokens.next(); + tokens.collect::() + } + false => tokens, + }; + return match syn::parse2::(tokens).ok() { + Some(type_name) => Some((type_name, deref)), + None => None + } + } + None +} + +/// Helper function to extract the type from the [`Attribute`], aka `#[armtype()]` +/// +/// Will return the raw [`Type`]. Useful for the [`Const`] and the [`ConstEach`] +/// macros +/// +/// # Input +/// +/// ```text +/// #[armtype()] +/// ``` +/// +/// # Output +/// +/// [`None`] if the attribute is not present / invalid +/// +/// Otherwise [`Some`] containing the type `` +fn get_type(attrs: &[Attribute]) -> Option { + for attr in attrs { + if !attr.path.is_ident("armtype") { continue; } + let tokens = match attr.parse_args::() { + Ok(tokens) => tokens, + Err(_) => return None, + }; + return syn::parse2::( + tokens + .into_iter() + .collect::() + ).ok() + } + None +} \ No newline at end of file diff --git a/impl/src/symbol.rs b/impl/src/symbol.rs new file mode 100644 index 0000000..f558c4a --- /dev/null +++ b/impl/src/symbol.rs @@ -0,0 +1,79 @@ +// -------------------------------------------------- +// external +// -------------------------------------------------- +use quote::ToTokens; + +// All symbols (all idents, e.g. no kebab-case) +pub(crate) const VALUE: Symbol = Symbol("value"); +pub(crate) const ARMTYPE: Symbol = Symbol("armtype"); + +/// Container symbols +pub(crate) static CONT_SYMBOLS: Symbols = Symbols(&[ + ARMTYPE +]); + +/// Variant symbols +pub(crate) static VARIANT_SYMBOLS: Symbols = Symbols(&[ + VALUE, +]); + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// A symbol for `thisenum` attributes +pub(crate) struct Symbol(pub(crate) &'static str); + +/// [`Symbol`] implementation of [`std::fmt::Display`] +impl std::fmt::Display for Symbol { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str(self.0) + } +} +/// [`Symbol`] implementation of [`From`] for [`syn::Path`] +impl From<&syn::Path> for Symbol { + fn from(path: &syn::Path) -> Self { + let ident = path.segments + .last() + .expect("path has no segments") + .ident + .to_string(); + Symbol(Box::leak(ident.into_boxed_str())) + } +} +/// [`syn::Path`] implementation of [`From`] for [`Symbol`] +impl From for syn::Path { + fn from(symbol: Symbol) -> Self { + syn::parse_str(symbol.0).expect("?") + } +} +/// [`Symbol`] implementation of [`ToTokens`] +impl ToTokens for Symbol { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + #[allow(clippy::unwrap_used)] // all symbols are idents + let ident: syn::Ident = syn::parse_str(self.0).unwrap(); + tokens.extend(quote::quote! { #ident }) + } +} +/// [`syn::Path`] implementation of [`PartialEq`] for [`Symbol`] +impl PartialEq for syn::Path { + fn eq(&self, word: &Symbol) -> bool { + self.is_ident(word.0) + } +} +/// [`&syn::Path`](syn::Path) implementation of [`PartialEq`] for [`Symbol`] +impl PartialEq for &syn::Path { + fn eq(&self, word: &Symbol) -> bool { + self.is_ident(word.0) + } +} + +/// Multiple symbols, for displaying errors +pub(crate) struct Symbols<'a>(&'a [Symbol]); + +/// [`Symbols`] implementation of [`std::fmt::Display`] +impl std::fmt::Display for Symbols<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut symbols = self.0.to_vec(); + // reverse alphabetical + symbols.sort_by(|a, b| b.0.cmp(a.0)); + symbols.iter().try_for_each(|symbol| write!(f, "`{}` ", symbol.0)) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8a58cff --- /dev/null +++ b/src/main.rs @@ -0,0 +1,233 @@ +use thisenum::{ + Const, + // ConstEach, +}; + +// #[derive(Const)] +// #[armtype(u8)] +// enum TestU8 { +// #[value = 0x7f] +// Arm1, +// #[value = 0x3B] +// Arm2, +// } + +// #[derive(Const)] +// #[armtype(&str)] +// enum TestStr { +// #[value = "this"] +// Arm1, +// #[value = "this"] +// Arm2, +// #[value = "this"] +// Arm3, +// #[value = "foo"] +// Arm4, +// #[value = "bar"] +// Arm5, +// #[value = "xD"] +// Arm6, +// #[value = "xD"] +// Arm7, +// } + +// #[derive(Const)] +// #[armtype(&[u8])] +// enum TestU8Slice4 { +// #[value = b"\x7F\x7F\x7F\x7F\x67"] +// Arm1(u8, u8, u8, u8), +// #[value = b"\x3B\x3B\x3B\x3B"] +// Arm2, +// } + +// #[derive(Const)] +// #[arm_type(Vec)] +// enum TestVecu8 { +// #[value = vec![1, 2, 3]] +// Arm1, +// #[value = vec![4, 5, 6]] +// Arm2, +// } + +// #[derive(ConstEach)] +// enum TestStrAny { +// #[armtype(u8)] +// #[value = 0xAA] +// Arm1, +// #[value = "test3"] +// Arm2, +// } + +fn main() { + + // main2(); + main3(); + + // // Const example + // assert_eq!(TestU8::Arm1.value(), &0x7F); + // assert_eq!(TestU8::Arm1, 0x7F as u8); + // assert_eq!(TestU8::Arm2.value(), &0x3B); + + // // Const example 2 + // assert_eq!(TestStr::Arm1.value(), "this"); + // assert_eq!(TestStr::Arm1, "this"); + // assert_eq!(TestStr::Arm2.value(), "that"); + + // // Const example 3 + // assert_eq!(TestU8Slice4::Arm1.value(), b"\x7F\x7F\x7F\x7F\x67"); + // assert_eq!(TestU8Slice4::Arm2.value(), b"\x3B\x3B\x3B\x3B"); + // assert_eq!(TestU8Slice4::Arm1, b"\x7F\x7F\x7F\x7F\x67" as &[u8]); + + // // ConstEach example + // assert!(TestStrAny::Arm1.value::().is_some()); + // let val = TestStrAny::Arm1.value::().unwrap(); + // println!("TestStrAny::Arm1.value() = {:?}", val); + // println!("TestStrAny::Arm1.value() = {:?}", TestStrAny::Arm1.value::().unwrap()); + // let value = TestStrAny::Arm2.value::>(); + // println!("TestStrAny::Arm2.value() = {:?}", value); + // assert!(TestStrAny::Arm2.value::>().is_none()); + // assert!(TestStrAny::Arm2.value::<&str>().is_some()); +} + +// #[derive(ConstEach, Debug)] +// enum MyEnum { +// #[armtype(u8)] +// #[value = 0xAA] +// A, +// #[value = "test3"] +// B, +// } + +// #[derive(ConstEach, Debug)] +// enum Tags { +// #[value = b"\x00\x01"] +// Key, +// #[armtype(u16)] +// #[value = 24250] +// Length, +// #[armtype(&[u8])] +// #[value = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"] +// Data, +// } + +// fn main2() { +// // [`ConstEach`] examples +// assert!(MyEnum::A.value::().is_some()); +// assert!(MyEnum::A.value::>().is_none()); +// assert!(MyEnum::B.value::().is_none()); +// assert!(MyEnum::B.value::<&str>().is_some()); + +// // An infered type. This will be as strict as possible, +// // therefore [`&[u8]`] will fail but [`&[u8; 2]`] will succeed +// assert!(Tags::Key.value::<&[u8; 2]>().is_some()); +// assert!(Tags::Key.value::<&[u8; 5]>().is_none()); +// assert!(Tags::Key.value::<&[u8]>().is_none()); +// assert!(u16::from_le_bytes(**Tags::Key.value::<&[u8; 2]>().unwrap()) == 0x0100); + +// // casting as anything other than the defined / inferred type will +// // fail, since this uses [`downcast_ref`] from [`std::any::Any`] +// assert!(Tags::Length.value::().is_some()); +// assert!(Tags::Length.value::().is_none()); +// assert!(Tags::Length.value::().is_none()); + +// // however, can always convert to a different type +// // after value is successfully acquired +// assert!(*Tags::Length.value::().unwrap() as u32 == 24250); +// } + +// use enum_const::Const; + +// #[derive(Const)] +// #[armtype(i32)] +// enum MyEnum { +// #[value = 0] +// A, +// #[value = 1] +// B, +// } + +// #[derive(Const)] +// #[armtype(&[u8])] +// enum Tags { +// #[value = b"\x00\x01\x7f"] +// Key, +// #[value = b"\xba\x5e"] +// Length, +// #[value = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"] +// Data, +// } + +// use enum_const::Const; + +#[derive(Const)] +#[armtype(&[u8])] +/// https://exiftool.org/TagNames/EXIF.html +enum ExifTag { + // ... + // #[value = b"\x01\xff"] + // ImageWidth2(T), + #[value = b"\x01\x00"] + ImageWidth(u8), + #[value = b"\x01\x01"] + ImageHeight, + #[value = b"\x01\x02"] + BitsPerSample, + #[value = b"\x01\x03"] + Compression, + #[value = b"\x01\x06"] + PhotometricInterpretation, + // ... +} + +// #[derive(ConstEach)] +// enum CustomEnum { +// #[armtype(&[u8])] +// #[value = b"\x01\x00"] +// A, +// #[value = "foo"] +// B, +// #[armtype(f32)] +// #[value = 3.14] +// C, +// } + +fn main3() { + // // it's prefered to use the function call to `value` + // // to get a [`&'static T`] reference to the value + // assert_eq!(MyEnum::A.value(), &0); + // assert_eq!(MyEnum::B.value(), &1); + // assert_eq!(Tags::Key.value(), b"\x00\x01\x7f"); + // assert_eq!(Tags::Length.value(), b"\xba\x5e"); + // assert_eq!(Tags::Data.value(), b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"); + + // // can also check equality without the function call. This must compare the input + // // type defined in `#[armtype = ...]` + // assert_eq!(Tags::Length, b"\xba\x5e"); + + // assert_eq!(ExifTag::ImageWidth.value(), b"\x01\x00"); + // #[cfg(feature = "eq")] + // assert_eq!(ExifTag::ImageWidth, b"\x01\x00"); + // println!("ExifTag::ImageWidth.value() = {:?}", ExifTag::ImageWidth.value()); + + // assert_eq!(CustomEnum::A.value::<&[u8]>().unwrap(), b"\x01\x00"); + // assert!(CustomEnum::B.value::<&str>().is_some()); + // assert_eq!(CustomEnum::B.value::<&str>().unwrap(), &"foo"); + // assert_eq!(CustomEnum::B.value::<&str>(), Some("foo").as_ref()); + // assert_eq!(CustomEnum::C.value::(), Some(3.14).as_ref()); + // // or on failure + // assert!(CustomEnum::C.value::().is_none()); + + // let my_enum = MyEnum2::Variant2(0x7F); + // let my_enum2 = MyEnum2::Variant3("foo".to_string(), (3.14, -0x7F)); + // match my_enum2 { + // MyEnum2::Variant2(x) => assert_eq!(x, 0x7F), + // MyEnum2::Variant3(_) => assert_eq!(x, "foo".to_string()), + // _ => panic!("should not happen"), + // } +} + +// enum MyEnum2 { +// Variant1, +// Variant2(u8), +// Variant3(String, (f32, i16)), +// } \ No newline at end of file