literify/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use {
4    proc_macro2::{Delimiter, Group, TokenStream, TokenTree},
5    quote::{quote, quote_spanned},
6    std::fmt::Write,
7};
8
9/// Stringify (and concat, also with strings) identifiers inside token stream
10#[proc_macro]
11pub fn literify(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
12    expand(ts.into()).into()
13}
14
15fn expand(ts: TokenStream) -> TokenStream {
16    let mut ts = ts.into_iter();
17    let mut out = TokenStream::default();
18
19    loop {
20        match ts.next() {
21            // 1. Found ~
22            Some(TokenTree::Punct(p)) if p.as_char() == '~' => match ts.next() {
23                // 2. Found group delimited by parentheses
24                Some(TokenTree::Group(grp)) if grp.delimiter() == Delimiter::Parenthesis => {
25                    let mut literal = String::new();
26
27                    // 3. Substitute tokens inside token stream
28                    for tt in grp.stream() {
29                        match tt {
30                            // Copy-in identifier
31                            TokenTree::Ident(id) => write!(literal, "{id}").unwrap(),
32
33                            // Copy-in string literal
34                            TokenTree::Literal(lit) => {
35                                let span = lit.span();
36                                if let litrs::Literal::String(lit) = litrs::Literal::from(lit) {
37                                    write!(literal, "{}", lit.value()).unwrap()
38                                } else {
39                                    return quote_spanned!(span => compile_error!("Unsupported literal"));
40                                }
41                            }
42
43                            // Copy-in punctuation
44                            TokenTree::Punct(punct) => write!(literal, "{punct}").unwrap(),
45                            
46                            // Other tokens are not supported
47                            tt => {
48                                return quote_spanned!(
49                                    tt.span() =>
50                                        compile_error!("Unsupported token tree (only string literals and identifiers are allowed)")
51                                )
52                            }
53                        }
54                    }
55
56                    out.extend(quote!(#literal));
57                }
58                // Recurse into groups
59                Some(TokenTree::Group(grp)) => out.extend([
60                    TokenTree::Punct(p),
61                    TokenTree::Group(Group::new(grp.delimiter(), expand(grp.stream()))),
62                ]),
63                Some(tt) => out.extend([TokenTree::Punct(p), tt]),
64                None => break,
65            },
66            // Recurse into groups
67            Some(TokenTree::Group(grp)) => out.extend([TokenTree::Group(Group::new(
68                grp.delimiter(),
69                expand(grp.stream()),
70            ))]),
71            Some(tt) => out.extend([tt]),
72            None => break,
73        }
74    }
75
76    out
77}