I'm starting playing with Rust macros and I came to try this little practice example. I want to define a macro that expands into a variable initialization (name doesn't matter) of type i32
(for example, but not really important) and a series of operations to that variable, in this case a var += 1
or a var -= 1
and finally it will call println!("{}", var)
.
The macro will take a series of tokens based on +
and -
that matches the operations described above.
So for example:
operate_integer![+++---]
would expand to:
let mut var: i32 = 0;
var += 1;
var += 1;
var += 1;
var -= 1;
var -= 1;
var -= 1;
print!("{}", var);
I decided to use 2 macros for this, one for wrapping the initialization and the printing and the other to evaluate the +-
tokens:
The base one would be:
macro_rules! operate_integer {
// $($all_tokens:tt)* should match everything, it will be forward to the helper macro
($($all_tokens:tt)*) => {
let mut var : i32 = 0;
operate_integer_helper![$($all_tokens:tt)*]
print!("{}", var);
}
}
The helper would expand the operations:
macro_rules! operate_integer_helper {
// the idea is that it matches a `+` followed by more tokens
(+$($t:tt)*) => {
val += 1;
operate_integer_helper![$($t:tt)*] // we recursively handle the remaining tokens
}
(-$($t:tt)*) => {
val -= 1;
operate_integer_helper![$($t:tt)*]
}
}
This of course do not work, it fails compilation with the following error (Playground):
error: no rules expected the token `(`
--> src/lib.rs:102:5
|
102 | (+$($t:tt)*) => {
| ^ no rules expected this token in macro call
I'm kind of stuck. I know I may be missing many concepts since I just started and I would really appreciate some help understanding how to work with macros. Thank you in advance!
You're actually very close! There are only a couple of minor errors left. (If you want to learn more about macros, only read one bullet point at a time and try to progress on your own from there!)
When using (repeated) meta variables, you don't specify the meta-variable type again. So it's $($t:tt)*
in the pattern of the macro, but if you want to use it, it's $($t)*
!
If you have multiple rules in a macro definition, you need to end every rule with a semicolon.
macro_rules! {
(+ $(t:tt)*) => { ... };
(- $(t:tt)*) => { ... };
}
The Rust compiler always needs to know whether you want to expand your macro into an expression or statement(s). Since you are generating a list of statements and not a single expression, you have to add a semicolon to the invocation of your macros! That means, in main()
but also all macro invocation of helper macros inside your macro definition.
Since yeah macro invocation creates a new syntax context and all identifiers (names) are only accessible in their syntax context, the helper macro cannot use var
(even after fixing the typo val
-> var
). So instead, you have to pass that name to the helper macro:
macro_rules! operate_integer {
($($all_tokens:tt)*) => {
let mut var: i32 = 0;
operate_integer_helper![var $($all_tokens)*]; // <-- pass identifier
println!("{}", var);
}
}
macro_rules! operate_integer_helper {
($var:ident +$($t:tt)*) => { // <- accept identifier
$var += 1; // <- use identifier
operate_integer_helper![$var $($t)*]
};
($var:ident -$($t:tt)*) => {
$var -= 1;
operate_integer_helper![$var $($t)*]
};
}
Having done all that you get the error "unexpected end of macro invocation". This is because you don't have a recursion stop rule! So you have to add a new rule to your helper macro: ($var:ident) => {};
. This rule is used when there is only the name and no +
or -
tokens left.
And now: it works!
I would still change one last thing: usually it's not a good idea to have a second helper macro, because that macro might not be in scope where the main macro is called. Instead, one usually uses internal rules. You can read more about those here.
With this, this is the resulting code.