Enum variant name as &str without Debug,Display,proc macro or heap allocation

I'm trying to find some generic way to get the variant name of an enum as &str, but in a restricted coding place where I cannot import another crate (so proc macros are off limits, since they require to be in their own crate), and Debug/Display are both used for something else, and I've to also not be using heap allocations (because I've to cover the case when I may be inside a fork-ed process).

I've found this (seemingly)hacky way of doing it with macro_rules! but it can only do enums that have either only tuple variants, OR only unit variants & struct variants. So it can't handle an enum that has all three. Can it be done though?(within the aforementioned constraints) that is my question. EDIT: yes, see at end of post. It's hacky though.

And I did this with ChatGPT 3.5 (the alternative being, none at all, so it's better with it than not at all, for me anyway).

Here's on playground complete code, or this is the macro:

// Define the VariantName trait
trait VariantNameAsStr {
    fn variant_name_as_str(&self) -> &str;
}
macro_rules! enum_str {
    //XXX: arm matches unit varians like Red and struct variants like Red { field1: i32, field2: i64, }, and a mixture of both is supported!
    ($(#[$attr:meta])* $vis:vis enum $name:ident $(<$($gen:ident),*>)?, $($variant:ident $({ $($field:ident: $ftype:ty),* $(,)? })?),* $(,)?) => {
        $(#[$attr])*
        $vis enum $name $(<$($gen),*>)? {
            $(
                $variant $({ $($field: $ftype),* })?
            ),*
        }//enum

        impl $(<$($gen),*>)? VariantNameAsStr for $name $(<$($gen),*>)? {
            fn variant_name_as_str(&self) -> &str {
                match self {
                    $(
                        // Handle variants with fields
                        Self::$variant $({ $($field: _),* })? => stringify!($variant),
                        //Self::$variant $({..})? => stringify!($variant),
                    )*
                }
            }//fn
        }//impl
    };

    //XXX: arm matches only tuple variants eg. Red(i32,i64,i128) but not Red, nor Red { field:i32 }, so you can't mix them!
    ($(#[$attr:meta])* $vis:vis enum $name:ident $(<$($gen:ident),*>)?, $($variant:ident $(($($ftype:ty),* $(,)? ))?),* $(,)?) => {
        $(#[$attr])*
            $vis enum $name $(<$($gen),*>)? {
                $(
                    $variant $(($($ftype),*))?,
                )*
            }//enum

        impl $(<$($gen),*>)? VariantNameAsStr for $name $(<$($gen),*>)? {
            fn variant_name_as_str(&self) -> &str {
                match self {
                    $(
                        Self::$variant(..) => stringify!($variant),
                    )*
                }
            }
        }//impl
    };//arm
} //macro

and example use:

enum_str! {
    pub enum Color,
    Red, Green, Blue,
    StructVariant1 {
        field1: i32,
    },
    //TupleVariant(i32),//XXX: can't match this here!
}

enum_str! {
    pub enum Color2<T,G>,
    //Tee { f: i32 }, // if u use this, then the tuple variant below isn't accepted!
    Red(T,G), Green(G,i32), Blue(i64,i128,),
    //Magenta,//XXX: this isn't accepted here!
    //Foo { field1: i32 }, //XXX: this isn't accepted here!
}
fn main() {
    let c=Color::Blue;
    assert_eq!(c.variant_name_as_str(),"Blue");
    let c2=Color2::<i128,&str>::Green("text",2);
    assert_eq!(c2.variant_name_as_str(),"Green");
}

And while writing this I've realized that if I get rid of that trait I can actually make that pub const fn variant_name_as_str(&self) -> &str which is definitely something I'm gonna done(playground)

EDIT: holy smokes, I think I may have found some way!
Ok this seems to work at first glance(playground):

macro_rules! replace_with_2_dots {
    ($($input:tt)*) => {
        ..
    };
}

macro_rules! enum_str {
    ($(#[$attr:meta])* $vis:vis enum $name:ident $(<$($gen:ident),*>)?,
    $(
        $variant:ident
            $( ( $($tfield:ty),* $(,)? ) )?
            $( { $($sfield:ident: $stype:ty),* $(,)? } )?
    ),* $(,)?
    ) => {
        $(#[$attr])*
        $vis enum $name $(<$($gen),*>)? {
            $(
                $variant $( ( $($tfield),* ) )?
                         $( { $($sfield: $stype),* } )?,
            )*
        }

        impl $(<$($gen),*>)? $name $(<$($gen),*>)? {
            fn variant_name_as_str(&self) -> &str {
                match self {
                    $(
                        Self::$variant $( ( replace_with_2_dots!( $($tfield),* ) ) )? $( { $($sfield: _),* } )? => stringify!($variant),

                    )*
                }
            }
        }
    };
}
enum_str! {
    pub enum Color,
    Red, Green, Blue,
    StructVariant1 { field1: i32 },
    TupleVariant(i32),
}

enum_str! {
    pub enum Color2<T, G>,
    Tee { f: i32 },
    Red(T, G), Green(G, i32), Blue(i64, i128),
    Magenta,
    Foo { field1: i32, field2: u8, },
}

fn main() {
    let c = Color::Blue;
    assert_eq!(c.variant_name_as_str(), "Blue");

    let c2 = Color2::<i128, &str>::Green("text", 2);
    assert_eq!(c2.variant_name_as_str(), "Green");

    let c3 = Color2::<i32,i32>::Magenta;
    assert_eq!(c3.variant_name_as_str(), "Magenta");

    let c4 = Color2::<u8,u8>::Foo { field1: 42, field2: 18 };
    assert_eq!(c4.variant_name_as_str(), "Foo");

    let c5 = Color::StructVariant1 { field1: 10 };
    assert_eq!(c5.variant_name_as_str(), "StructVariant1");

    let c6 = Color::TupleVariant(10);
    assert_eq!(c6.variant_name_as_str(), "TupleVariant");
}

expansion is this:

pub enum Color {
    Red,
    Green,
    Blue,
    StructVariant1 {
        field1: i32,
    },
    TupleVariant(i32),
}
impl Color {
    fn variant_name_as_str(&self) -> &str {
        match self {
            Self::Red => "Red",
            Self::Green => "Green",
            Self::Blue => "Blue",
            Self::StructVariant1 { field1: _ } => "StructVariant1",
            Self::TupleVariant(..) => "TupleVariant",
        }
    }
}
pub enum Color2<T, G> {
    Tee {
        f: 
        i32,
    },
    Red(T, G),
    Green(G, i32),
    Blue(i64, i128),
    Magenta,
    Foo {
        field1: i32,
        field2: u8,
    },
}
impl<T, G> Color2<T, G> {
    fn variant_name_as_str(&self) -> &str {
        match self {
            Self::Tee { f: _ } => "Tee",
            Self::Red(..) => "Red",
            Self::Green(..) => "Green",
            Self::Blue(..) => "Blue",
            Self::Magenta => "Magenta",
            Self::Foo { field1: _, field2: _ } => "Foo",
        }
    }
}

I've stumbled upon some inconsistency between rustc and rust-analyzer while doing this: `rust-analyzer` shows compile error E0023 but `cargo check`/clippy/build/run do not · Issue #125464 · rust-lang/rust · GitHub

Some improvements(not seen above):
I got rid of matching lone comma, see isolated example here
Tried to match a part of the generics but it requires duplication of matcher block, here
While work-in-progress, updates should be here, after done(if ever), I'll make new post.

1 Like

Perhaps something like strum::IntoStaticStr could meet your needs. The attribute macro doesn't seem to allocate, and returns a &'static str. Best if all, since it's a macro, it's fairly light on maintenance.

3 Likes

That macro looks just about right. A trivial change adding some curly braces in the pattern makes the code a little nicer to read.

1 Like

I'm still working on it offline, but it's rather difficult(wip) to try to match the where clause of the enum, but I'm trying to make it support any enum, except empty ones which would require each match to have a default arm because I don't know how to make the whole impl block optional because it uses $variant already inside it and I get some error about already repeating at this depth.

I'll post code updates here [1] and I'll make a new post here, on urlo, when I think I'm done with the result.

But I think anyone else stumbling upon this would be better served by the above post by jjpe (thanks!), but for me, since I can't use external crates, this macro_rules! version is really good!

Thanks all!


  1. (it's a perma link, don't forget to switch to main branch, but just in case I do directory renames in the future, i made it permalink so it won't 404) ↩︎

you don't have to invent custom syntax, you can use macro_rules to parse normal rust enum definitions and extract the variant names. it is relative easy to parse common use cases, but if you want to cover every corner cases, it can get tedious and frustrating.

below is an example if you are only interested in the variant name (ignore the fields if any), it can parse common cases, but I didn't bother to make it perfectly robust. feel free to modify it to suit your need.

/// the macro name is arbitrarily chosen
/// ```
/// x! {
/// 	/// an example enum definition
/// 	pub enum Foo {
/// 		VariantX,
/// 		VariantY(i32),
/// 		VariantZ {
/// 			value: i32
/// 		}
/// 	}
/// }
/// ```
macro_rules! x {
	// "internal" entry point with saved raw tokens
	// matches enum definition, start recursion with empty accumulator
	{
		(@raw $($raw:tt)*)
		$(#[$_attr:meta])*
		$_vis:vis
		enum $E:ident {
			$($variants:tt)*
		}
	} => {
		 x! {
			(@raw $($raw)*)
			(@enum $E)
			(@variants)
			(@recur $($variants)*)
		 }
	};
	// final expansion, terminates recursion
	// simply emit original tokens, plus a `variant_name()` method
	{
		(@raw $($raw:tt)*)
		(@enum $E:path)
		(@variants $($V:ident)*)
		(@recur $(,)?)
	} => {
		// first emit original raw tokens
		$($raw)*
		// then implement `variant_name()` method
		impl $E {
			pub fn variant_name(&self) -> &'static str {
				match self {
					$(
						Self:: $V {..} => stringify!($V)
					),*
				}
			}
		}
	};
	// recursive invoke the same macro - order matters!
	// variant with named fields, capture variant name, ignore fields
	{
		(@raw $($raw:tt)*)
		(@enum $E:path)
		(@variants $($V:ident)*)
		(@recur
			$(,)?
			$V0:ident { $($_:tt)* }
			$($recur:tt)*
		)
	} => {
		x! {
			(@raw $($raw)*)
			(@enum $E)
			(@variants $($V)* $V0)
			(@recur $($recur)*)
		}
	};
	// variant with tuple fields, capture variant name, ignore fields
	{
		(@raw $($raw:tt)*)
		(@enum $E:path)
		(@variants $($V:ident)*)
		(@recur
			$(,)?
			$V0:ident ( $($_:tt)* )
			$($recur:tt)*
		)
	} => {
		x! {
			(@raw $($raw)*)
			(@enum $E)
			(@variants $($V)* $V0)
			(@recur $($recur)*)
		}
	};
	// fieldless variant:
	// variant with tuple fields, capture variant name, ignore fields
	{
		(@raw $($raw:tt)*)
		(@enum $E:path)
		(@variants $($V:ident)*)
		(@recur
			$(,)?
			$V0:ident
			$($recur:tt)*
		)
	} => {
		x! {
			(@raw $($raw)*)
			(@enum $E)
			(@variants $($V:ident)* $V0)
			(@recur $($recur)*)
		}
	};
	// catches error case, otherwise it will fallthrough to infinite recusrion
	{
		(@raw $($raw:tt)*)
		$($tt:tt)+
	} => {
		compile_error!("failed to parse enum definition")
	};
	// "real" entry point: matches and captures everything, must come last
	// this is not robust, if called
	($($raw:tt)*) => {
		x! {
			(@raw $($raw)*)
			$($raw)*
		}
	};
}
1 Like

Wow, I did not expect that { .. } would work to ignore the fields regardless of the variant type used!

Is there a tutorial on that macro/recursion stuff? I'm trying to get your example to work(outdated). Seems difficult to know where it errored, if the macro is made this way?

That compile_error! is a nice touch.

laterEDIT: I see you've used the so called "internal rules" and NOT what it's seen at the very beginning of the The Little Book of Rust Macros which says "@ has a purpose, though most people seem to forget about it completely: it is used in patterns to bind a non-terminal part of the pattern to a name." (so, it's NOT this).

https://veykril.github.io/tlborm/

2 Likes

oh good, that single url in a post, with no tittle or anything [1] looked suspiciously like spam/phishing/whatever, but it turns out it's just the rendered version of:

Thanks!


  1. although when I quote it here above it gets a title magically ↩︎

sorry, there's typo in my example, the macro is intended to parse a normal enum definition but I missed the enum keyword in the comment:

 enum_str! {
 	/// doc string ...
-	pub Foo {
+	pub enum Foo {
 		// variants ...
 	}
 }

you can play with it yourself by tweaking the enum definition. e.g one limittation of the current rule is only the enum's attributes (including doc comments) is accepted, while attributes (thus doc comments) of variants are rejected.

the key to debug macros is to see the expansion in steps. for this, the -Zmacro-backtrace compiler flags (unstable, requires nightly toolchain) is very handy.

but sometimes, even -Zmacro-backtrace can't give enough context, especially for recursive rules (which is usually the case for parsing related maros), in which case I usually manually "unroll" the expected expansion order level by level using "internal" rules and see which "level" causes the problem.

that's just one trick to avoid infinite recusion in failed expansions. alternatively you can make the "matches all tokens" rule a separate macro, which also remove the possiblity of infinite recursion. the trade off is you now have two macro names to export. e.g.

macro_rules! x {
  //...
}
macro_rules! y {
  ($($tt:tt)*) => {
    x! {
      (@raw $($tt)*)
      $($tt)*
    }
  };
}
1 Like

I'm sorry, it had a rendered preview when I composed the answer.

this might be why:

Introduction - The Little Book of Rust Macros
Sorry, we were unable to generate a preview for this web page, because the following oEmbed / OpenGraph tags could not be found: description, image

that's in the preview of it, so I guess when posted it's just no preview then.

Looks like this has been done far better than I could've ever written[1] here:

see that enum-as-str link right in the readme, even though it requires other crates, it's still macro_rules based all in all.

For my simple use case, I don't really need a macro that can support any kind of enum definition, so a simple one like jorendorff mentioned will do instead. Others who don't care about it being macro_rules based, can try the proc macros one mentioned by jjpe.


  1. and fail to in the process ↩︎

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.