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 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:
#[const_impl(const)]
trait ConstantId {
const ID: i32;
const HI: u128;
}
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
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.
TIL macro_rules!
can be used inside a #[...]
https://docs.rs/macro_rules_attribute/ is necessary for that.
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; }
const __: ()
generic parameter 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
}
}
+ )?
)*
)}
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).
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.
::macro_rules_attribute::apply
? (#[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 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:
#[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!