Reputation: 1876
I'm trying to write a macro that automatically expands a set of enum variants into a builder (base on a previous question, though I don't think that's relevant). Basically, I want to pass it a value, a builder struct and a list of enum variants and generate match arms. The result should be equivalent to this:
match a {
Attribute::InsuranceGroup { value } => builder.insurance_group(value),
};
I think I've managed to get fairly close, but I can't figure out how to convert the UpperCamelCase InsuranceGroup
to lower_camel_case insurance_group
using the casey crate, in order to call the builder method. Currently, I have:
macro_rules! unwrap_value {
($var:ident, $builder:ident, [ $( $enum:ident :: $variant:ident ),+ $(,)? ]) => {
match $var {
$($enum::$variant { value } => $builder.snake!($variant) (value),)*
}
}
}
unwrap_value! {
a, builder, [Attribute::InsuranceGroup]
}
However, this fails at $builder.snake!($variant)
with the analyser complaining that it expects (
, ,
, .
, ::
, ?
, }
or an operator, instead of !
.
I also tried moving snake!
outside, snake!($builder.$variant)
, but then it says it can't find $builder
in this scope.
While I would be interested in any suggestions of alternatives to using the builder which would eliminate this problem, I'm more interested in understanding what I'm doing wrong with the macros in order to better understand them.
Upvotes: 0
Views: 331
Reputation: 70850
While I would strongly recomment using the paste
crate (23,999,580 total downloads vs 7,611 for casey
as the time of writing, paste
is even available on the playground!), I will explain here why casey
didn't work for the sake of knowledge (and suggest a solution! But one you shouldn't use).
The first version didn't work because macros cannot be used after the dot. You can check that out easily:
macro_rules! m {
() => { m };
}
v.m!();
error: expected one of `(`, `.`, `::`, `;`, `?`, `}`, or an operator, found `!`
--> src/main.rs:6:4
|
6 | v.m!();
| ^ expected one of 7 possible tokens
Neither they will be allowed here in the future (probably), because this will collide (and confuse) with the possibility for postfix macros in the future. This is stated by the reference, too: no MacroInvocation is allowed after the dot in MethodCallExpr, TupleIndexingExpr or FieldExpr.
The natural solution is to wrap the whole call in macros (insert the (value)
also there, because otherwise it considers that as if you've written (v.method)(value)
and that is invalid method call): snake!($builder.$variant(value))
.
However, now the compiler complains with a strange error:
error[E0425]: cannot find value `builder` in this scope
--> src\lib.rs:6:44
|
6 | $($enum::$variant { value } => snake!($builder.$variant(value)),)*
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: a unit struct with a similar name exists: `Builder`
...
17 | struct Builder;
| --------------- similarly named unit struct `Builder` defined here
...
26 | / unwrap_value! {
27 | | a, builder, [Attribute::InsuranceGroup]
28 | | }
| |_____- in this macro invocation
|
= note: this error originates in the macro `snake` (in Nightly builds, run with -Z macro-backtrace for more info)
What? WHAT?? Surely builder
is in scope! We declared it! Just straight before the macro call (me at least)!
The result is macro hygiene.
How does snake!()
generates its identifiers? Well, let's dive into the code...
Ident::new(&transform(ident.to_string()), Span::call_site())
Hmm, let's go to the docs of Span::call_site()
...
The span of the invocation of the current procedural macro. Identifiers created with this span will be resolved as if they were written directly at the macro call location (call-site hygiene) and other code at the macro call site will be able to refer to them as well.
(Emphasis mine. "The macro" here refers to the calling proc-macro, i.e. snake!()
in this case).
And indeed, we can reproduce this error if we just spell out builder
ourselves. Because the macro doesn't see builder
. It can only see variables it created. In fact, using call-site hygiene here is a bug of casey
. paste
does the correct thing and uses the hygiene of the original identifier.
In fact, this just reveals a bigger problem with our approach: what if the variable will not be snake-cased already? It should be, but if not, we will snake-case it incorrectly!
But what can we do? The paste
crate gives us proper control over what identifiers we want to change, but casey
does not (another reason to choose paste
)! Can we walk against our proc macro?
Can we??
Maybe. I see at least one approach that doesn't require us to do that (find it yourself!). But I actually do want to work against our proc-macro. Try at least. Because why not? It's funny.
snake!()
doesn't look into groups. That means, it will change something like AbC
- but not something like (AbC)
. So we can just wrap $builder
in parentheses...
$($enum::$variant { value } => snake!(($builder).$variant(value)),)*
(No need to handle value
because it is already snake-cased, and originates in our macro).
And it works! Voilà! (You should not depend on that. This may be a bug and subject to changes.)
Upvotes: 1
Reputation: 1876
Well after hunting for hours I finally found a solution shortly after posting. Instead of using the casey crate I'm using the paste crate, which includes the functionality I need. The macro code then becomes:
macro_rules! unwrap_value {
($var:ident, $builder:ident, [ $( $enum:ident :: $variant:ident ),+ $(,)? ]) => {
match $var {
$($enum::$variant { value } => paste!($builder.[<$variant:snake>](value)) ,)*
}
}
}
Upvotes: 1