Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I store an identifier (`proc_macro::Ident`) as a constant to avoid repeating it?

I am writing a procedural macro and I need to emit a very long identifier multiple times (potentially because of hygiene, for example). I use quote! to create TokenStreams, but I don't want to repeat the long identifier over and over again!

For example, I want to generate this code:

let very_long_ident_is_very_long_indeed = 3;
println!("{}", very_long_ident_is_very_long_indeed);
println!("twice: {}", very_long_ident_is_very_long_indeed + very_long_ident_is_very_long_indeed);

I know that I can create an Ident and interpolate it into quote!:

let my_ident = Ident::new("very_long_ident_is_very_long_indeed", Span::call_site());
quote! {
    let #my_ident = 3;
    println!("{}", #my_ident);
    println!("twice: {}", #my_ident + #my_ident);
}

So far so good, but I need to use that identifier in many functions all across my code base. I want it to be a const that I can use everywhere. However, this fails:

const FOO: Ident = Ident::new("very_long_ident_is_very_long_indeed", Span::call_site());

With this error:

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --> src/lib.rs:5:70
  |
5 | const FOO: Ident = Ident::new("very_long_ident_is_very_long_indeed", Span::call_site());
  |                                                                      ^^^^^^^^^^^^^^^^^

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --> src/lib.rs:5:20
  |
5 | const FOO: Ident = Ident::new("very_long_ident_is_very_long_indeed", Span::call_site());
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I doubt that those functions will be marked const any time soon.

I could make the string itself a constant:

const IDENT: &str = "very_long_ident_is_very_long_indeed";

But then wherever I want to use the identifier, I need to call Ident::new(IDENT, Span::call_site()), which would be pretty annoying. I want to just write #IDENT in my quote! invocation. Can I somehow make it work?

like image 405
Lukas Kalbertodt Avatar asked Mar 02 '23 23:03

Lukas Kalbertodt


1 Answers

Fortunately, there is a way!

The interpolation via # in quote! works via the ToTokens trait. Everything implementing that trait can be interpolated. So we just need to create a type that can be constructed into a constant and which implements ToTokens. The trait uses types from proc-macro2 instead of the standard proc-macro one.

use proc_macro2::{Ident, Span, TokenStream};


struct IdentHelper(&'static str);

impl quote::ToTokens for IdentHelper {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        Ident::new(self.0, Span::call_site()).to_tokens(tokens)
    }
}

Now you can define your identifier:

const IDENT: IdentHelper = IdentHelper("very_long_ident_is_very_long_indeed");

And directly use it in quote!:

quote! {
    let #IDENT = 3;
}

(Full example)

like image 131
Lukas Kalbertodt Avatar answered Mar 05 '23 16:03

Lukas Kalbertodt