[proc-macro] getting generic parameters in enum fields

Hi,

Is there anyway to extract the generic parameters in the fields of an enum variant using syn?

Like this:

enum Hello<T, U> {
    Tee(T),
    You(U)
}

Is there a way that I can extract the generic parameter T from the first variant and U from the second?

I'm trying to generate struct that matches the fields of the variant, we would get structs like:

struct FirstVariant<T>(T);
struct SecondVariant<U>(U);

I'm writing a derive macro, so I can get all the generics from DeriveInput, but I can't figure out how to get the individual generics. All syn eposes for each field of each variant is a Type, but it doesn't say if the Type is generic over anything, and if so, what that generic parameter is.

Thank you for your help!

Procedural macros don't have access to type checking since they run before type checking does. You can manually compare the types in the variants to see if they match the generic parameter name (iirc they should be Paths with a single Ident if they're generic parameters).

It's not fun, but if you know what your use case is use can just throw an error if the enum does anything more complicated (like use a reference to a generic parameters instead of a value) and that simplifies things a bit at least.

Ok gotchu.

What if I just stuck some unnecessary PhantomDatas with the correct generic parameters.

So we would get something like:

struct<T, U> FirstVariant(T, PhantomData<T, U>);
struct<T, U> SecondVariant(U, PhantomData<T, U>);

So some of the PhantomData's generics would be extraneous, but we wouldn't need to figure out types as we can use syn's TypeGenerics. Do you know if this would have any effects on variance and typey stuff?

Nevermind, PhantomData only takes one generic parameter so this doesn't work :frowning:

You could do that with PhantomData<*const (T, U)> but that will have implications for the usability of the structs since they'll have a bunch of extra type parameters which can't be inferred easily in other contexts.

Ahh ok

By reading this code, you, as a human developer, can say that this type Hello is an enum with two generic parameters T and U and its first variant Tee only uses the parameter T etc. How did you know that? Just like you, proc macro derives can also read this code. All you need to do is to translate your in-brain algorithm into proc macro code.

enum Hello<T, U> means the name T and U are generic parameters. If a variant mentions that names it contains mentioned generic parameters.

1 Like

I'm not sure how to do that through syn's API though, and I don't want to hand write a parser for enums. I think I could start by parsing the ToTokens output of the TypeGenerics struct from syn, since it doesn't have any useful methods.

I also completely forgot that lifetimes fall into the same category as type generics parsing-wise, so supporting generics might be more effort than is worth it.

:100:


@fprasx whilst there are some very contrived scenarios where parsing, alone, is unable to tell you which parameters are definitely used, in practice, nobody would write code that contrived, or, at least, you could have extra optional annotations be required by those wanting something that crazy.

  • To clarify, I'm talking of things like <T as IdentityIgnoring<U>::Itself which, for some conveniently defined trait IdentityIgnoring<U>, would resolve to T, and thus ignore U, despite featuring both T and U (this is, by the way, they way to write type aliases which disregard parameters).

So, in 99.9% of the cases, if a type mentions / names another type inside it, it's a fair heuristic to consider that type as being used.

And in order to do that, you could use a Visitor:

use {
    ::core::{
        ops::Not,
    },
    ::syn::{*,
        visit::{self as subrecurse, Visit},
    },
};

fn generics_for_ty<'generics> (
    generics: &'generics Generics,
    ty: &'_ Type,
) -> Vec<&'generics Ident>
{
    struct GenericsVisitor<'a> {
        unseen_types: Vec<&'a Ident>,
    }
    
    impl<'i> Visit<'i> for GenericsVisitor<'_> {
        fn visit_type_path(&mut self, ty_path: &'i TypePath) {
            subrecurse::visit_type_path(self, ty_path);
            if ty_path.qself.is_some() { return; }
            // if the type path is made of a single ident:
            if let Some(ident) = ty_path.path.get_ident() {
                self.unseen_types
                    .retain(|&generic| ident != generic)
            }
        }
    
        fn visit_item(&mut self, _: &'i Item) {
           /* do not subrecurse */
        }
    }

    let generic_tys = || generics.type_params().map(|it| &it.ident);
    let mut visitor = GenericsVisitor {
        unseen_types: generic_tys().collect(),
    };
    visitor.visit_type(ty);
    generic_tys()
        .filter(|ty| {
            visitor
                .unseen_types
                .contains(ty)
                .not()
        })
        .collect()
}
  • and ditto for const generics by visiting ExprPaths.

Quid of the lifetimes?

For the lifetimes you can use your PhantomData idea, since lifetimes cannot cause type inference errors.

1 Like

Wow thank you for the demonstration! I'll try this out.