cairo_lang_quote/
lib.rs

1use std::iter::Peekable;
2
3use proc_macro2::{Delimiter, Ident, Span, TokenTree};
4
5extern crate proc_macro;
6use quote::quote as rust_quote;
7use ra_ap_rustc_parse_format::{ParseError, ParseMode, Parser, Piece, Position};
8use syn::parse::{Parse, ParseStream};
9use syn::punctuated::Punctuated;
10use syn::{Error, Expr, LitStr, Token, parse_macro_input};
11
12#[derive(Debug)]
13#[cfg_attr(test, derive(PartialEq))]
14enum QuoteToken {
15    Var(Ident),
16    Content(String),
17    Whitespace,
18}
19
20enum DelimiterVariant {
21    Open,
22    Close,
23}
24
25impl QuoteToken {
26    pub fn from_delimiter(delimiter: Delimiter, variant: DelimiterVariant) -> Self {
27        match (delimiter, variant) {
28            (Delimiter::Brace, DelimiterVariant::Open) => Self::Content("{".to_string()),
29            (Delimiter::Brace, DelimiterVariant::Close) => Self::Content("}".to_string()),
30            (Delimiter::Bracket, DelimiterVariant::Open) => Self::Content("[".to_string()),
31            (Delimiter::Bracket, DelimiterVariant::Close) => Self::Content("]".to_string()),
32            (Delimiter::Parenthesis, DelimiterVariant::Open) => Self::Content("(".to_string()),
33            (Delimiter::Parenthesis, DelimiterVariant::Close) => Self::Content(")".to_string()),
34            (Delimiter::None, _) => Self::Content(String::default()),
35        }
36    }
37}
38
39fn process_token_stream(
40    mut token_stream: Peekable<impl Iterator<Item = TokenTree>>,
41    output: &mut Vec<QuoteToken>,
42) {
43    // Rust proc macro parser to TokenStream gets rid of all whitespaces.
44    // Here we just make sure no two identifiers are without a space between them.
45    let mut was_previous_ident: bool = false;
46    while let Some(token_tree) = token_stream.next() {
47        match token_tree {
48            TokenTree::Group(group) => {
49                let token_iter = group.stream().into_iter().peekable();
50                let delimiter = group.delimiter();
51                output.push(QuoteToken::from_delimiter(
52                    delimiter,
53                    DelimiterVariant::Open,
54                ));
55                process_token_stream(token_iter, output);
56                output.push(QuoteToken::from_delimiter(
57                    delimiter,
58                    DelimiterVariant::Close,
59                ));
60                was_previous_ident = false;
61            }
62            TokenTree::Punct(punct) => {
63                if punct.as_char() == '#' {
64                    // Only peek, so items processed with punct can be handled in next iteration.
65                    if let Some(TokenTree::Ident(ident)) = token_stream.peek() {
66                        if was_previous_ident {
67                            output.push(QuoteToken::Whitespace);
68                        }
69                        let var_ident = Ident::new(&ident.to_string(), Span::call_site());
70                        output.push(QuoteToken::Var(var_ident));
71                        was_previous_ident = true;
72                        // Move iterator, as we only did peek before.
73                        let _ = token_stream.next();
74                    } else {
75                        // E.g. to support Cairo attributes (i.e. punct followed by non-ident `#[`).
76                        output.push(QuoteToken::Content(punct.to_string()));
77                        was_previous_ident = false;
78                    }
79                } else {
80                    output.push(QuoteToken::Content(punct.to_string()));
81                    was_previous_ident = false;
82                }
83            }
84            TokenTree::Ident(ident) => {
85                if was_previous_ident {
86                    output.push(QuoteToken::Whitespace);
87                }
88                output.push(QuoteToken::Content(ident.to_string()));
89                was_previous_ident = true;
90            }
91            TokenTree::Literal(literal) => {
92                output.push(QuoteToken::Content(literal.to_string()));
93                was_previous_ident = false;
94            }
95        }
96    }
97}
98
99#[proc_macro]
100pub fn quote(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
101    let mut output_token_stream = rust_quote! {
102      let mut quote_macro_result = ::cairo_lang_macro::TokenStream::empty();
103    };
104
105    let input: proc_macro2::TokenStream = input.into();
106    let token_iter = input.into_iter().peekable();
107    let (size_hint_lower, _) = token_iter.size_hint();
108    let mut parsed_input: Vec<QuoteToken> = Vec::with_capacity(size_hint_lower);
109    process_token_stream(token_iter, &mut parsed_input);
110
111    for quote_token in parsed_input.iter() {
112        match quote_token {
113            QuoteToken::Content(content) => {
114                output_token_stream.extend(rust_quote! {
115                  quote_macro_result.push_token(::cairo_lang_macro::TokenTree::Ident(::cairo_lang_macro::Token::new(::std::string::ToString::to_string(#content), ::cairo_lang_macro::TextSpan::call_site())));
116                });
117            }
118            QuoteToken::Var(ident) => {
119                output_token_stream.extend(rust_quote! {
120                  quote_macro_result.extend(::cairo_lang_macro::TokenStream::from_primitive_token_stream(::cairo_lang_primitive_token::ToPrimitiveTokenStream::to_primitive_token_stream(&#ident)).into_iter());
121                });
122            }
123            QuoteToken::Whitespace => output_token_stream.extend(rust_quote! {
124              quote_macro_result.push_token(::cairo_lang_macro::TokenTree::Ident(::cairo_lang_macro::Token::new(" ".to_string(), ::cairo_lang_macro::TextSpan::call_site())));
125            }),
126        }
127    }
128    proc_macro::TokenStream::from(rust_quote!({
129      #output_token_stream
130      quote_macro_result
131    }))
132}
133
134struct QuoteFormatArgs {
135    fmtstr: LitStr,
136    args: Punctuated<Expr, Token![,]>,
137}
138
139impl Parse for QuoteFormatArgs {
140    fn parse(input: ParseStream) -> syn::Result<Self> {
141        let fmtstr = input.parse::<LitStr>()?;
142
143        let args = if input.peek(Token![,]) {
144            let _ = input.parse::<Token![,]>()?;
145            Punctuated::parse_terminated(input)?
146        } else {
147            Punctuated::new()
148        };
149
150        Ok(QuoteFormatArgs { fmtstr, args })
151    }
152}
153
154/// Basic tokenizer for `quote_format!` macro.
155///
156/// Intentionally simplified to avoid full parsing of Cairo syntax.
157/// Only splits strings into tokens and preserves whitespace.
158/// Token kinds and spans are ignored as spans always set to call site.
159/// Additionally, it's expected placeholders (`{}`, `{0}`, etc.) are already
160/// stripped out by the format parser before this function is called.
161fn tokenize_basic(string: &str) -> Vec<QuoteToken> {
162    string
163        .split(char::is_whitespace)
164        .map(|s| QuoteToken::Content(s.to_string()))
165        .flat_map(|content| [QuoteToken::Whitespace, content])
166        .skip(1)
167        .collect()
168}
169
170/// Build a Cairo TokenStream from a string literal with format placeholders.
171///
172/// Unlike `quote!` macro, this macro bypasses Rust's parser,
173/// allowing Cairo-specific syntax that is not valid Rust syntax.
174///
175/// Unlike `quote!` macro, this macro does not support token `#token` interpolation.
176/// Placeholders are substituted with arguments implementing `ToPrimitiveTokenStream`.
177/// Supported format placeholders are: `{}`, `{0}`, `{1}`, etc.
178#[proc_macro]
179pub fn quote_format(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
180    let QuoteFormatArgs { fmtstr, args } = parse_macro_input!(input as QuoteFormatArgs);
181    let fmtsrc = fmtstr.value();
182    let args: Vec<&Expr> = args.iter().collect();
183
184    let mut output_token_stream = rust_quote! {
185      let mut quote_macro_result = ::cairo_lang_macro::TokenStream::empty();
186    };
187    let mut parser = Parser::new(&fmtsrc, None, None, false, ParseMode::Format);
188
189    for piece in &mut parser {
190        match piece {
191            Piece::Lit(string) => {
192                for token in tokenize_basic(string) {
193                    match token {
194                        QuoteToken::Content(content) => {
195                            output_token_stream.extend(rust_quote! {
196                              quote_macro_result.push_token(::cairo_lang_macro::TokenTree::Ident(::cairo_lang_macro::Token::new(::std::string::ToString::to_string(#content), ::cairo_lang_macro::TextSpan::call_site())));
197                            });
198                        }
199                        // Vars are handled via placeholders, so they should not appear here.
200                        QuoteToken::Var(_) => {
201                            unreachable!("tokenizer cannot return a var quote token type")
202                        }
203                        QuoteToken::Whitespace => {
204                            output_token_stream.extend(rust_quote! {
205                              quote_macro_result.push_token(::cairo_lang_macro::TokenTree::Ident(::cairo_lang_macro::Token::new(" ".to_string(), ::cairo_lang_macro::TextSpan::call_site())));
206                            });
207                        }
208                    }
209                }
210            }
211            Piece::NextArgument(arg) => {
212                let expr = match arg.position {
213                    Position::ArgumentIs(idx) | Position::ArgumentImplicitlyIs(idx) => {
214                        if let Some(expr) = args.get(idx).copied() {
215                            expr
216                        } else {
217                            return Error::new(
218                                fmtstr.span(),
219                                format!(r#"format arg index {} is out of range (the format string contains {} args)."#,
220                                idx,
221                                args.len()
222                                )
223                            )
224                                .to_compile_error()
225                                .into();
226                        }
227                    }
228                    Position::ArgumentNamed(name) => {
229                        return Error::new(
230                            fmtstr.span(),
231                            format!(
232                                "named placeholder '{{{}}}' is not supported by this macro.\nhelp: use positional ('{{}}') or indexed placeholders ('{{0}}', '{{1}}', ...) instead.",
233                                name
234                            ),
235                        )
236                        .to_compile_error()
237                        .into();
238                    }
239                };
240                output_token_stream.extend(rust_quote! {
241                  quote_macro_result.extend(
242                    ::cairo_lang_macro::TokenStream::from_primitive_token_stream(::cairo_lang_primitive_token::ToPrimitiveTokenStream::to_primitive_token_stream(&#expr)).into_iter()
243                  );
244                });
245            }
246        }
247    }
248    if !parser.errors.is_empty() {
249        let ParseError {
250            description,
251            note,
252            label,
253            span: _,
254            secondary_label: _,
255            suggestion: _,
256        } = parser.errors.remove(0);
257        let mut err_msg = format!("failed to parse format string: {label}\n{description}");
258        if let Some(note) = note {
259            err_msg.push_str(&format!("\nnote: {note}"));
260        }
261        return Error::new(fmtstr.span(), err_msg).to_compile_error().into();
262    }
263    proc_macro::TokenStream::from(rust_quote!({
264      #output_token_stream
265      quote_macro_result
266    }))
267}
268
269#[cfg(test)]
270mod tests {
271    use super::{QuoteToken, process_token_stream};
272    use proc_macro2::{Ident, Span};
273    use quote::{TokenStreamExt, quote as rust_quote};
274
275    #[test]
276    fn parse_cairo_attr() {
277        let input: proc_macro2::TokenStream = rust_quote! {
278            #[some_attr]
279            fn some_fun() {
280
281            }
282        };
283        let mut output = Vec::new();
284        process_token_stream(input.into_iter().peekable(), &mut output);
285        assert_eq!(
286            output,
287            vec![
288                QuoteToken::Content("#".to_string()),
289                QuoteToken::Content("[".to_string()),
290                QuoteToken::Content("some_attr".to_string()),
291                QuoteToken::Content("]".to_string()),
292                QuoteToken::Content("fn".to_string()),
293                QuoteToken::Whitespace,
294                QuoteToken::Content("some_fun".to_string()),
295                QuoteToken::Content("(".to_string()),
296                QuoteToken::Content(")".to_string()),
297                QuoteToken::Content("{".to_string()),
298                QuoteToken::Content("}".to_string()),
299            ]
300        );
301    }
302
303    #[test]
304    fn quote_var_whitespace() {
305        /*
306        Construct program input, equivalent to following:
307        input = rust_quote! {
308            #[some_attr]
309            mod #name {
310            }
311        }
312        In a way that avoids `#name` being parsed as `rust_quote` var.
313        */
314        let mut input: proc_macro2::TokenStream = rust_quote! {
315            #[some_attr]
316            mod
317        };
318        input.append(proc_macro2::TokenTree::Punct(proc_macro2::Punct::new(
319            '#',
320            proc_macro2::Spacing::Joint,
321        )));
322        input.extend(rust_quote! {
323            name {
324            }
325        });
326        let mut output = Vec::new();
327        process_token_stream(input.into_iter().peekable(), &mut output);
328        assert_eq!(
329            output,
330            vec![
331                QuoteToken::Content("#".to_string()),
332                QuoteToken::Content("[".to_string()),
333                QuoteToken::Content("some_attr".to_string()),
334                QuoteToken::Content("]".to_string()),
335                QuoteToken::Content("mod".to_string()),
336                QuoteToken::Whitespace,
337                QuoteToken::Var(Ident::new("name", Span::call_site())),
338                QuoteToken::Content("{".to_string()),
339                QuoteToken::Content("}".to_string()),
340            ]
341        );
342    }
343
344    #[test]
345    fn interpolate_tokens() {
346        use super::{QuoteToken, process_token_stream};
347        use proc_macro2::{Ident, Punct, Spacing, Span, TokenTree};
348        use quote::{TokenStreamExt, quote as rust_quote};
349
350        // impl #impl_token of NameTrait<#name_token> {}
351
352        let mut input: proc_macro2::TokenStream = rust_quote! {
353            impl
354        };
355        input.append(TokenTree::Punct(Punct::new('#', Spacing::Joint)));
356        input.extend(rust_quote! {
357            impl_token
358        });
359        input.extend(rust_quote! {
360            of NameTrait<
361        });
362        input.append(TokenTree::Punct(Punct::new('#', Spacing::Joint)));
363        input.extend(rust_quote! {
364            name_token> {}
365        });
366
367        let mut output = Vec::new();
368        process_token_stream(input.into_iter().peekable(), &mut output);
369        assert_eq!(
370            output,
371            vec![
372                QuoteToken::Content("impl".to_string()),
373                QuoteToken::Whitespace,
374                QuoteToken::Var(Ident::new("impl_token", Span::call_site())),
375                QuoteToken::Whitespace,
376                QuoteToken::Content("of".to_string()),
377                QuoteToken::Whitespace,
378                QuoteToken::Content("NameTrait".to_string()),
379                QuoteToken::Content("<".to_string()),
380                QuoteToken::Var(Ident::new("name_token", Span::call_site())),
381                QuoteToken::Content(">".to_string()),
382                QuoteToken::Content("{".to_string()),
383                QuoteToken::Content("}".to_string()),
384            ]
385        );
386    }
387}