Keeping a state within a procedural macro

I'm currently writing a procedural macro that can be used like this:

#[with_implementor(name = "impl_int_feature")]
/// An algebra containing the integer feature
pub trait IntAlg<T> {
    /// Create an integeral literal
    fn int(&self, i: isize) -> T;
}

This macro will create the trait and will create a macro named impl_int_feature to easily implement this trait when using composition.

The following code can then be written to easily implement the above trait for a type using composition.

struct ComposedStruct {
	int_helper: IntStruct, // IntStruct does implement IntAlg<T>
}
impl_int_feature!(ComposedStruct, int_helper, T);

This works fine. But now I would like to create a second procedural macro that can automatically create a struct and do the implementation:

language!(IntBoolLanguage<E> for IntAlg<E> as int_helper, BoolAlg<E> as bool_helper);

should create the following code:

struct IntBoolLanguage<E, T1: IntAlg<E>, T2: BoolAlg<E>> {
	int_helper: T1,
	bool_helper: T2
}
impl_int_feature!(IntBoolLanguage, int_helper, E);
impl_bool_feature!(IntBoolLanguage, bool_helper, E);

For this, I need to somehow get the macro_names (impl_int_feature and impl_bool_feature) from the passed traits (IntAlg, BoolAlg).

I currently do this, by keeping track of the macro_names in a HashMap within the proc_macro code. I add the name within the with_implementor macro and read it in the language macro. This works, but I need to make sure that all traits annotated with with_implementor are compiled before the language macros as the hashmap is otherwise not yet filled.

Is their an alternative way to keep track of this state without relying on the compilation order?

Are you sure it really works or is it works in cases I care about? Because I'm quite surprised you have no issues with rust-analyzer, IntelliJ Rust plugin and so on.

No, if you macros depend on the order in which they are called then it's your responsibility to track changes in tools and keep them working.

I don't think Rust language team would call start expanding proc macro in a random order any time soon (although it's an interesting idea and may be worth implementing), but if/when they would do that it wouldn't be considered a breaking change.

One option would be for your original macro to produce a trait definition like this:

pub trait IntAlg<T> {
    type Delegate: IntAlg<T>;
    fn _delegate(&self)->&Self::Delegate { panic!("self-recursion!") }

    fn int(&self, i: isize) -> T { self._delegate().int(i) }
}

Then, manual implementations implement int(), etc. with type Delegate = Self; and your derived implementations can override _delegate(). The biggest downside here is that the compiler won't complain if you forget to override one of the methods.


You could fix this with two traits and a blanket implementation, but then you're back to the problem of sending extra names between proc macro implementations. You could pick an enforced naming convention, though, that is unlikely to cause problems:

pub trait IntAlg<T> {
    /// Create an integeral literal
    fn int(&self, i: isize) -> T;
}

pub trait IntAlg__Delegate<T> {
    type Delegate: IntAlg<T> + ?Sized;
    fn delegate(&self)->&Self::Delegate;
}

impl<T, U:IntAlg__Delegate<T>+?Sized> IntAlg<T> for U {
    fn int(&self, i: isize)->T { <Self as IntAlg__Delegate<T>>::delegate(self).int(i) }
}

And then your example language!(...) call would expand to this:

struct IntBoolLanguage<E, T1, T2> {
	int_helper: T1,
	bool_helper: T2
}

impl<E, T1, T2> IntAlg__Delegate<E> where T1: IntAlg<T> {
    type Delegate = T1;
    fn delegate(&self)->&T1 { &self.int_helper }
}

impl<E, T1, T2> BoolAlg__Delegate<E> where T2: BoolAlg<T> {
    type Delegate = T2;
    fn delegate(&self)->&T2 { &self.bool_helper }
}
2 Likes

It seems that crate inwelling can help collecting all traits annotated with with_implementor then generate a traits.rs file in OUT directory. The crate structx demonstrates the usage of crate inwelling.

Thanks for all the replies. I do now have multiple ideas for alternative implementations in case my current solutions breaks (due to a different proc_macro expansion order).

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.