Generate a macro

I have a trait

trait ConstGet<T> {
  fn get() -> T
}

and I want to generate an attribute-like macro to wrap all const in the trait ConstantId<T> into a ConstGet<T>

trait ConstantId {
  const ID: i32;
  const HI: u128;
} 

I don't understand what you are trying to do. Can you please post:

  1. usage of macro
  2. expected output when macro is expanded
  1. usage of macro
#[const_impl(const)]
trait ConstantId {
  const ID: i32;
  const HI: u128;
}
  1. expected output when macro is expanded
impl ConstGet<i32> for i32 {
  fn get() -> i32 
}

I am not a macro expert, but I think you will end up needing Procedural Macros - The Rust Reference rather than (the much easier to use) macro_rules! - Rust By Example

2 Likes

Procedural macros seem to be very powerful, as if I understand it right, they can analyze the token stream of the annotated trait, for example, and use a touring-complete mechanism (Rust) to rewrite it. I missed simple examples in the docs that would teach me how to do it, but maybe I should just look closer into the reference and the compiler-provided proc_macro crate.

That doesn't compile: what would the body of get() be?

By inferring your desire to be more like:

#[apply(derive_Gets!)]
trait ConstantId {
    const ID: i32;
    const HI: u128; // <- assuming distinct types since those are used for the lookup?
}

to emit:

impl<T : ?Sized + ConstantId> Get<i32> for T {
    const GET: i32 = <T as ConstantId>::ID;
    // and/or
    fn get ()
      -> i32
    {
        <T as ConstantId>::ID
    }
}
impl<T : ?Sized + ConstantId> Get<u128> for T {
    const GET: u128 = <T as ConstantId>::HI;
}
// etc.

Which could be implemented as:

macro_rules! derive_Gets {(
    $( #[doc = $doc:expr] )*
    $pub:vis
    trait $TraitName:ident {
        $(
            const $CONST_NAME:ident : $ConstTy:ty ;
        )*
    }
) => (
    $( #[doc = $doc] )*
    $pub
    trait $TraitName {
        $(
            const $CONST_NAME : $ConstTy;
        )*
    }

    $(
        impl<__T : ?::core::marker::Sized + $TraitName>
            $crate::path::to::ConstGet< $ConstTy >
        for
            __T
        {
            const GET: $ConstTy = <Self as $TraitName>::$CONST_NAME;
            // and/or
            fn get ()
              -> $ConstTy
            {
                <Self as $TraitName>::$CONST_NAME
            }
        }
    )*
)}

If the trait definition is more complex, then indeed using an actual procedural macro could be warranted.

6 Likes

TIL macro_rules! can be used inside a #[...]

1 Like

https://docs.rs/macro_rules_attribute/ is necessary for that.

1 Like

what if the macro was applied in the trait instead for a single const?

trait ConstantId {
  #[apply(derive_gets!)]
  const ID: i32;
}

It could be done, but there are two caveats:

  • the macro does not know where it is called, so it cannot know about the ConstantId trait, which is a needed element for the expansion. But if the trait is always gonna be ConstantId, you technically could hack your way around it by hard-coding the ConstantId name (and path) in the expansion;

  • even assuming that you have the contents that you wish to emit, the context where such items will be emitted will be inside that trait definition.

    That is, for instance, say you wanted to emit an impl ... definition.

    Then,

    would become:

    trait ConstantId {
      const ID: i32;
      impl ...
    }
    

    which is invalid Rust.

    So, you'd need to somehow squeeze unrelated item definitions while inside a trait definition that you don't want to mess up with, and having only access to const <identifier>: <ty>;.

    Looks impossible, and any reasonable person would leave it there, but just for the fun here is how one would do it:

    //! Macro helper(s)
    trait GimmeAConstScope<const B: bool> { type T : ?Sized; }
    impl<T : ?Sized, const B: bool> GimmeAConstScope<B> for T { type T = Self; }
    
    • For some reason, one can't have a const __: () generic parameter :sob:

    With it, one can write:

    trait ConstantId {
        const ID: <i32 as $crate::path::to::GimmeAConstScope<{
            impl ...
    
            true || false // whichever you prefer :)
        }>>::T;
    }
    

All that to say that having the attribute on the associated items only is not the best way forward.


A way easier approach is to keep the attribute on the trait definition, but require a helper annotation on the specific items that you wish to handle. That is, the call-site could look like this:

#[apply(derive_Gets!)]
trait ConstantId {
    #[Get]
    const ID: i32;

    const HI: u128;
}

And have the outer macro skip the associated items with no #[Get] applied to them. Here is how our previous macro_rules! implementation would have to be tweaked:

macro_rules! derive_Gets {(
    $( #[doc = $doc:expr] )*
    $pub:vis
    trait $TraitName:ident {
        $(
+           $(#[Get $(@$Get:tt)?])?
            const $CONST_NAME:ident : $ConstTy:ty ;
        )*
    }
) => (
    $( #[doc = $doc] )*
    $pub
    trait $TraitName {
        $(
            const $CONST_NAME : $ConstTy;
        )*
    }

    $(
+     $($($Get)?
        impl<__T : ?::core::marker::Sized + $TraitName>
            $crate::path::to::ConstGet< $ConstTy >
        for
            __T
        {
            const GET: $ConstTy = <Self as $TraitName>::$CONST_NAME;
            // and/or
            fn get ()
              -> $ConstTy
            {
                <Self as $TraitName>::$CONST_NAME
            }
        }
+     )?
    )*
)}
  • Playground

  • Bonus for the more curious

    This "nesting everything under a $( ... )? repetition" approach can easily run into trouble if you have another $( ... )* repetition inside it (e.g., imagine ( $($ConstTy:tt)* ) rather than $ConstTy:ty). In that case, it helps to avoid wrapping the stuff insider the $()? repetition, and, instead, to emit a prefix. It will be the role of the prefix to cancel the whole thing it applies to (or not), by using, you guessed it, #[cfg]s.

    The expansion would thus then be:

    #[cfg(any(
        $($($Get)? all() )?
    ))]
    impl ...
    

    Since when #[Get] is provided, it yields #[cfg(any(all()))], which is a no-op (always-true cfg), and otherwise yields #[cfg(any())], which removes the item it is applied to (always-false cfg).


1 Like

This what I get when running

error: no rules expected the token `#`
  --> src/main.rs:11:9
   |
11 |         #[Get]
   |         ^ no rules expected this token in macro call
...
18 |     macro_rules! derive_Gets {(
   |     ------------------------ when calling this macro

also can't figure out what this is for?

$( #[doc = $doc:expr] )*

That's to support doc strings:

/// Some text

is actually sugar that gets converted, when parsing, to:

#[doc = " Some text"]

Another option would have been to use $( #[$attr:meta] )*, to support any attribute (e.g., #[cfg(…)]s).


Hmm, hard to diagnose the issue without a snippet of what you fed to the macro. My playground link above worked, so I'm pretty confident the macro works for simple inputs.

  • Maybe it's a bug in ::macro_rules_attribute::apply? (:thinking: the #[Get] attr contents maybe get passed by Rust as an opaque :meta blob to the attribute, and thus, to derive_Gets!, making the comparison against a literal Get fail :thinking: ).
    EDIT: I've done some testing on my own, and this doesn't seem to be the case.

Prefer this the previous syntax, is there a way to run unit tests?

As a unit-test, you could write:

#[test]
fn test_derive_gets ()
{
    #[apply(derive_Gets!)]
    trait Foo {
        const BOO: ();

        #[Get]
        const HI: i32;

        const OOB: usize;
    }

    enum Bar {}
    impl Foo for Bar { const HI: i32 = 42; const BOO: () = (); const OOB: usize = 0; }

    assert_eq!(<Bar as ConstGet<i32>>::GET, 42);
    // and/or
    assert_eq!(<Bar as ConstGet<i32>>::get(), 42);

    ::static_assertions::assert_not_impl_any!(Bar : ConstGet<()>, ConstGet<usize>);
}

where the crate

https://lib.rs/static_assertions

is needed so as to write an assertion regarding a lack of an implementation (another option would be to generate a `compile_fail!` test, but those are more brittle (what if the test code starts failing to compile for another reason?), and would require `derive_Gets` to be `#[macro_export]`ed, so a const/compile-time assertion of a lack of an impl is better.

my code

#[macro_use]
extern crate macro_rules_attribute;

pub trait ConstGet<T> {
    fn get() -> T;
}

fn main() {
    #[macro_export]
    macro_rules! derive_Gets {(
            $( #[doc = $doc:expr] )*
            $pub:vis
            trait $TraitName:ident {
                $(
                    $(#[Get $(@$Get:tt)?])?
                    const $CONST_NAME:ident : $ConstTy:ty ;
                )*
            }
    ) => (
        $( #[doc = $doc] )*
        $pub
        trait $TraitName {
            $(
                const $CONST_NAME : $ConstTy;
            )*
        }

        $(
        $($($Get)?
          impl<__T : ?::core::marker::Sized + $TraitName>
          $crate::path::to::ConstGet< $ConstTy >
          for
          __T
          {
              fn get ()
                  -> $ConstTy
                  {
                      <Self as $TraitName>::$CONST_NAME
                  }
        }
        )?
        )*
    )}
    
    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn test_derive_gets ()
        {
            #[apply(derive_Gets!)]
            trait Foo {
                const BOO: ();

                #[Get]
                const HI: i32;

                const OOB: usize;
            }

            enum Bar {}
            impl Foo for Bar { const HI: i32 = 42; const BOO: () = (); const OOB: usize = 0; }

            assert_eq!(<Bar as ConstGet<i32>>::get(), 42);
        }
    }
}

the error

warning: cannot test inner items
  --> src/main.rs:60:9
   |
60 |         #[test]
   |         ^^^^^^^
   |
   = note: `#[warn(unnameable_test_items)]` on by default
   = note: this warning originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)

warning: `macro-demo` (bin "macro-demo" test) generated 1 warning
    Finished test [unoptimized + debuginfo] target(s) in 0.01s
     Running unittests (target/debug/deps/macro_demo-83f00cbd98bf1bb5)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

For the test driver to be able to invoke the test in the first place, it needs to be accessible when ignoring privacy. It isn't possible to access functions nested inside another function, so #[test] inside a function can't work. You will have to move it outside of the function.

where would i place it?

Move the mod tests { … } outside fn main() { … } (and the macro definition now that we are at it).


Regarding

That path::to was a placeholder (I forgot to mention that); in your case, you'd thus end up with the following:

Click to expand
#[macro_use]
extern crate macro_rules_attribute;

pub trait ConstGet<T> {
    fn get() -> T;
}

macro_rules! derive_Gets {(
    $( #[doc = $doc:expr] )*
    $pub:vis
    trait $TraitName:ident {
        $(
            $(#[Get $(@$Get:tt)?])?
            const $CONST_NAME:ident : $ConstTy:ty ;
        )*
    }
) => (
    $( #[doc = $doc] )*
    $pub
    trait $TraitName {
        $(
            const $CONST_NAME : $ConstTy;
        )*
    }

    $(
        $($($Get)?
            impl<__T : ?::core::marker::Sized + $TraitName>
                $crate::path::to::ConstGet< $ConstTy >
            for
                __T
            {
                fn get ()
                  -> $ConstTy
                {
                    <Self as $TraitName>::$CONST_NAME
                }
            }
        )?
    )*
)}
pub(in crate) use derive_Gets;

fn main() {
    println!("Hello, World!");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_derive_gets() {
        #[apply(derive_Gets!)]
        trait Foo {
            const BOO: ();

            #[Get]
            const HI: i32;

            const OOB: usize;
        }

        enum Bar {}
        impl Foo for Bar {
            const HI: i32 = 42;
            const BOO: () = ();
            const OOB: usize = 0;
        }

        assert_eq!(<Bar as ConstGet<i32>>::get(), 42);
    }
}

Would it be okay if a drop a link to a PR I'm working on, related to the questions I asked and the solutions you gave?

Sure!