New Rustacean e010: Macros rule!

:arrow_forward: e010: Macros rule!—coming in with another punny title (it's been a while since I managed a really good one; had to up my game again)! This week, I talked about using Rust's macro-by-example system, and discussed a few of the limitations and things we might see coming in the future for them.

Macros rule! macro_rules! Whichever you prefer. :wink:

As always, comments, critique, clarifications, and pull requests appreciated.

3 Likes

When you use a macro, you actually transform one syntax tree into a different syntax tree.

This is true, but only technically. The input to a macro is a sequence of token trees, not syntax trees. See the Source Analysis: Token Trees section of TLBoRM for diagrams. The important part here is that the input to a macro doesn't have to make any damned sense whatsoever to the compiler, beyond having correct nesting and not containing any invalid tokens. Just in case you don't believe me: Ook!.

They're stable.

Hahahahahahaaaaa (sobs)

...they operate on the abstract syntax tree that the compiler has generated.

Again, not really. They operate primarily on tokens, but the macro can ask the parser to extract an AST node from the token stream; the AST node then turns into a special kind of token. Captures and Expansion Redux goes into this a bit (with more diagrams).

Other macros can be written as standalone items (~11:08)

This vaguely implies that you can write ident macros (name! some_ident { body }) macros with macro_rules!: you can't. You can only create those with compiler plugins.

(talking about try_main!)

You should make the paths absolute; ::std::result::etc rather than std::result::etc. Those paths will only work in the root module, or where std has been explicitly brought into local scope. Here's an example showing how relative std paths don't work everywhere.

... not the first thing you should reach for.

Lies.

1 Like

This! This right here is what I knew was going to happen when I started working on this episode. (And it. is. glorious. Lest I be misunderstood.)

So, first of all, thank you, @DanielKeep. I'll link this for people from the Twitter account and mention it on the next show. These clarifications are incredibly helpful. I read as widely as I could, but obvious still missed a fair bit.

Second, a few questions provoked by your responses:

This is true, but only technically. The input to a macro is a sequence of token trees, not syntax trees.... The important part here is that the input to a macro doesn't have to make any damned sense whatsoever to the compiler, beyond having correct nesting and not containing any invalid tokens.

Oh, interesting. So if I understand you correctly here, the only requirement is that what comes out of the macro makes sense to the compiler? That's… interesting if so. (By "interesting" you should read me as meaning "uhhhhhhhh".)

Hahahahahahaaaaa (sobs)

:confused: I'd love it if you elaborated on this. I know that there are definitely plans to massively improve the system, and I read Nick Cameron's series in detail (as you probably gathered), so I'm familiar with the things he wants to do re:macro! replacing macro_rules! and stabilizing procedurals, etc. But I didn't see anything to indicate that macro_rules! macros were themselves unstable—I thought the whole point of stabilizing them for 1.0 (even though they're deeply unsatisfactory in certain ways) was, well… so that we'd have something stable. Did I misunderstand you, or the history of the project, or both?

Again, not really. They operate primarily on tokens, but the macro can ask the parser to extract an AST node from the token stream; the AST node then turns into a special kind of token. Captures and Expansion Redux goes into this a bit (with more diagrams).

/me goes off to read this discussion and presumably to have his brain melt

This vaguely implies that …

Ah, dagnabbit. I misunderstood the summary in the Rust Reference.

All of this sort of leaves me asking: is there anywhere official we have these things more thoroughly documented? (@steveklabnik?) I'd be delighted to help with that effort, even if just by asking lots of questions/saying the wrong thing repeatedly on a podcast. :stuck_out_tongue_winking_eye:

You should make the paths absolute; ::std::result::etc rather than std::result::etc. Those paths will only work in the root module, or where std has been explicitly brought into local scope.

Ah, I was wondering about that. I also ran into a bit of a quirk when I tried to $crate::std::result::etc, but I didn't have time to chase it down if I were going to get out the door in time for my flight this morning. Argh. I'll have to (a) fix that in the show notes and (b) see if I can figure out why it wasn't working right.

Lies.

I blame the Rust Book. So it's probably Steve's fault. :wink:

Seriously, many thanks; this is super helpful. (As I said in the first episode: I'm using this as a way of helping myself learn Rust, as well as a service to the community.)

Yes. In particular the result must be a complete and (syntactically) valid expression, statement(s), item(s), impl item(s), or pattern. In an upcoming release (forget which), you can also expand to a type.

After all, if the input had to make sense, then the only thing you could feed to a macro would be already-valid syntax, which would rather limit what you could do with them, especially when you consider that macros possess no AST deconstruction abilities whatsoever.

macro_rules! is a broadly working pile of code held together with preschool craft supplies. You know, the sort of tape and glue that doesn't so much bind two things together as mildly disincline them to come apart... unless you look at them too hard.

In particular, there are two salient things macro_rules! is bad at:

  1. It sucks at preventing you from writing macros that can parse code which might change meaning in the future. It makes some efforts to this end, but there are seriously holes big enough to drive a small planet through.

  2. It sucks at giving you control over parsing.

The latest issue is type ascription: this takes the form expr : ty. This is a problem because there are rules like this in the wild:

macro_rules! totally_legit {
    ($e:expr) => {...};
    ($i:ident : $t:ty) => {...};
}

Macros such as the above used to work, but no longer do, thus violating backward compatibility (and consequently, stability).

What sucks about this change in particular is that previously, there were ways to get around changes to macro parsing. This one, however, is 100% unfixable. If you have a macro that depends on rules like that, your only option is to throw out the macro and start again, you can't "fix" the rules such that existing code will continue to parse with the same meaning.

I might sound bitter about this. It's not at all because I've been actively working on a macro that relied on exactly this very rule and had to take an axe to it because of the change.

Nope.

...

eye twitches

Just to be clear: I'm sympathetic to the developer's position in all this. They have two options here: halt language changes until macro_rules! gets replaced by something less dodgy (which could take who-knows how long), or sacrifice macro back-compat for the sake of highly desired language changes.

They're kinda damned if they do, damned if they don't.

...

Still pissed, though.

/me goes off to read this discussion and presumably to have his brain melt

So long as you don't go reading the source to custom_derive!, you should be fine. Or anything written by IcyFoxy.

Ah, I was wondering about that. I also ran into a bit of a quirk when I tried to $crate::std::result::etc, but I didn't have time to chase it down if I were going to get out the door in time for my flight this morning. Argh. I'll have to (a) fix that in the show notes and (b) see if I can figure out why it wasn't working right.

$crate expands to either nothing or ::crate_name where crate_name is the imported name of the crate the macro comes from. So, for macros defined in std, it expands to nothing if the macro is used inside the defining crate.

If you're referring to a type in std, just use ::std:: as the prefix everywhere. If you're referring to a type in another crate, your best bet is to publicly re-export the necessary names in your own crate, then access them using $crate::.

No problem.

2 Likes

The macros chapter in the book and the reference are the only two places, really. Help appreciated, for sure.