quote_into

Crates.ioquote_into
lib.rsquote_into
version0.2.0
sourcesrc
created_at2023-01-06 19:16:44.754061
updated_at2023-01-07 21:11:43.523236
descriptionEasily & efficiently generate code by quoting it in a macro
homepage
repository
max_upload_size
id752523
size16,280
Reinis Mazeiks (rMazeiks)

documentation

README

A fast code generation macro

Easily & efficiently generate code by quoting it:

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut stream = TokenStream::new();
quote_into!(stream += println!("hello world!"));
assert_eq!(stream.to_string(), "println ! (\"hello world!\")");

Variable interpolation

You can interpolate any value that implements ToTokens using the variable's name prefixed with #:

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut stream = TokenStream::new();
let variable = false;
quote_into!(stream += let boolean = #variable;);
assert_eq!(stream.to_string(), "let boolean = false ;");

Interpolation blocks

You can insert a block of Rust code using #{}. Inside, you can make function calls, use conditionals, or loops, or anything else allowed in a code block.

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut stream = TokenStream::new();
quote_into!(stream += let array = [#{
    for i in 0..3 {
        quote_into!(stream += Some(#i),)
    }
}];);
assert_eq!(stream.to_string(), "let array = [Some (0i32) , Some (1i32) , Some (2i32) ,] ;");

Note: if you have a group in the quoted tokens (such as {}, (), or []), a new TokenStream is created. In the example above, the outer stream is not the same as the stream inside the #{} interpolation, since it is wrapped in [].

Expression interpolation

You can also interpolate any Rust expression using #():

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut s = TokenStream::new();
quote_into!(s += const ANSWER: u32 =  #(7 * 6););
assert_eq!(s.to_string(), "const ANSWER : u32 = 42i32 ;");

Comparison with quote

The de-facto standard for code generation is the quote crate. This crate proposes small improvements to its API and performance.

API

Quote seems to be largely inspired by macro_rules when it comes to loop interpolations. While expressive and concise, they are a bit hard to learn and read in my opinion.

Instead, quote_into provides a simple "interpolation block" #{ } where you can write regular Rust code. Since there is basically no additional syntax, using interpolation blocks should feel more intuitive.

Performance

quote returns a new TokenStream, while quote_into accepts a TokenStream to append tokens to. While this makes quote a bit less verbose, it's also slightly less efficient when it comes to combining quoted fragments. Often, code generation happens in different functions which are then combined. The recommended way to do this is to create intermediate TokenStreams, and interpolate them using #:

use quote::quote;

let type_definition = quote! {...};
let methods = quote! {...};

let tokens = quote! {
    #type_definition
    #methods
};

The additional allocations have a small performance impact even if you only create a few TokenStreams and combine them. A more extreme case is a deeply recursive function, which repeatedly constructs and interpolates many token streams. For example, consider the below functions, which generate code of the form Box<Box<...<bool>>>:

use quote::quote;
use quote_into::quote_into;
use proc_macro2::TokenStream;

// Using quote:
fn quote_box_wrap(levels: usize) -> TokenStream {
    match levels {
        0 => quote!(bool),
        other => {
            let inner = quote_box_wrap(other - 1);
            quote!(Box<#inner>)
        }
    }
}

// Using quote_into:
fn quote_into_box_wrap(levels: usize) -> TokenStream {
    fn inner(s: &mut TokenStream, levels: usize) {
        match levels {
            0 => quote_into!(s += bool),
            other => quote_into! {s += Box < #{
                inner(s, other - 1)
            } >},
        }
    }

    // there's a bit of boilerplate, but only once per macro
    let mut s = TokenStream::new();
    inner(&mut s, levels);
    s
}

Results for levels=100:

quote:      591.14 µs
quote_into: 17.247 µs

Since the version using quote has to create many intermediate TokenStreams, it is much slower in this case. Again, this is an extreme example. It's unlikely your code generation will actually be 30x faster if you switch to quote_into, unless you're inefficiently nesting interpolated TokenStreams several levels deep as above.

Status: prototype

While you can technically use quote_into in your projects, it's only in the prototype stage.

  • The API is subject to change, as there are a few things I'm unsure about
  • It's implemented using quote... I'm not sure how "bad" this is (it might increase compile times), but I have a feeling it should be rewritten using manual token generation code at some point.
Commit count: 0

cargo fmt