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 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 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 let _ = token_stream.next();
74 } else {
75 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
154fn 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#[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 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 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 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}