Use attribute proc macro to generate enum variants

Suppose I have a type MyEnumVariantGen which impl IntoIterator<Item=(String,u8)>, I can write a proc macro to generate variants for #[repr(u8)] enum MyEnum.

But, if I want to generate variants for enum MyEnum2 with MyEnum2VariantGen, I'd have to clone & modify the existing macro, and the implementations of both MyEnumVariantGen and MyEnum2VariantGen must live in the proc macro crate, not the main crate.

Is there any way I can implement MyEnumVariantGen/MyEnum2VariantGen in the main crate?

If you mean you can write a macro that generates MyEnumVariantGen given the definition of MyEnum as input, then yes. If you mean a macro that generates MyEnum given MyEnumVariantGen, then no. Macros can't use runtime code in your crate like that.

Derive macros can define a custom attribute which you can use to provide arguments to the macro and change their behaviour. I can't see any reason you can't have a single macro generate both.

Yes, rewrite them as declarative macros. If you still want to use a procedural macro, then no.

This is what I'm trying to do. But the outcome is really buggy.

I'll skip the MyEnumVariantGen custom Iterator part, 'cause the macro expansion doesn't work as expected even without it.

This is a minimal demo of the proc macro crate:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Variant};

#[proc_macro_attribute]
pub fn gen_variants(_: TokenStream, input: TokenStream) -> TokenStream {
    let mut ast: DeriveInput = parse_macro_input!(input);

    if let Data::Enum(ref mut data_enum) = ast.data {
        if !data_enum.variants.is_empty(){
            return quote! {
                compile_error!("This macro can only be used with empty enums");
            }
            .into()
        }
        data_enum.variants.extend::<[Variant; 3]>([
            syn::parse_quote! { A = 101 },
            syn::parse_quote! { B = 102 },
            syn::parse_quote! { C = 103 },
        ]);
        quote! {
            #ast
        }
    } else {
        quote! {
            compile_error!("This macro can only be used with enums");
        }
    }
    .into()
}

(I'm not familiar with proc macros, this is based on Copilot generated code)

And in the main crate:

#[gen_variants]
enum MyEnum {}

Problem #1

IDE (RustRover, to be specific) doesn't recognize the generated variants.

In fact, it doesn't recognize the manipulated enum at all:

So of course IDE wouldn't help me generate the match arms.

However, this code actually compiles and works.

Problem #2

RustRover isn't the only one who fails to cooperate. The Debug derive macro fails to cover the macro-generated variants too:


So the problem is, the compiler clearly recognized the altered enum definition, but a lot of other components in the toolchain didn't.

Would the situation be improved in the near future?


P.S.

Could you give me a hint on how that can be done?

I just want to say: this whole approach is quite strange. If you're generating the contents of the enum, why not just generate the entire thing? Instead of an attribute macro you attach to an empty stub (which is clearly confusing your IDE), a simple functional macro that outputs the entire definition would probably be a lot simpler. Something like:

gen_enum!();
// expanding to...
#[derive(Debug)]
enum MyEnum { A = 101, B = 102, C = 103 }

Have you tried switching the order of the attributes so #[gen_variants] comes first? Presumably the Debug derivation is being expanded before the enum has anything in it.

I wrote that assuming you were doing this the other way around: using MyEnum to generate the iterator type. I cannot imagine going the other way with a declarative macro.

You're right, switching the order does make #[derive(Debug)] work - I previously thought it's either "innermost macro expanded first" or "attribute macros always before derive macros".

This isn't correctly expanded by RustRover either:

#[proc_macro]
pub fn gen_enum(_: TokenStream) -> TokenStream {
    (quote! {
        #[derive(Debug)]
        enum MyEnum { A = 101, B = 102, C = 103 }
    })
    .into()
}

Seems it's just RustRover failing. VSCode/rust-analyzer could get the definition of MyEnum right, no matter if it's generated by an attribute macro or a functional macro. I'm switching to VSCode now.

I'll miss the convenient macro expansion via hover info / context menu, but for the time being, a less convenient functionality is better than a broken one.


Edit: although I can't get macro expansion via hover info, I can get the actual type definition like this:

variant hover info

Interesting.

I believe Rust generally follows the rule that macro expansion is always outside-first and, thereby, macros are never given the output of other macros. That way, macros don't have to worry about how other macros might read and act on their expanded code; it can stay an implementation detail.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.