telety_impl/
command.rs

1use std::borrow::Cow;
2
3use proc_macro2::{Punct, Spacing, Span, TokenStream};
4use quote::{format_ident, quote, quote_spanned, ToTokens};
5use syn::{parse_quote, parse_quote_spanned, spanned::Spanned as _, Ident, LitInt, Path};
6
7use crate::{find_and_replace::SingleToken, Telety};
8
9pub(crate) type GenerateMacroTokens = fn(tele_ty: &Telety) -> Option<TokenStream>;
10
11/// Used to invoke the telety-generated macro in a manageable way.
12pub struct Command {
13    version: usize,
14    keyword: &'static str,
15    generate_macro_tokens: GenerateMacroTokens,
16}
17
18impl Command {
19    pub(crate) const fn new(
20        version: usize,
21        keyword: &'static str,
22        generate_macro_tokens: GenerateMacroTokens,
23    ) -> Self {
24        Self {
25            version,
26            keyword,
27            generate_macro_tokens,
28        }
29    }
30
31    pub(crate) const fn version(&self) -> usize {
32        self.version
33    }
34
35    fn version_lit(&self, span: Option<Span>) -> LitInt {
36        let span = span.unwrap_or(Span::call_site());
37        LitInt::new(&self.version().to_string(), span)
38    }
39
40    fn keyword(&self, span: Option<Span>) -> Ident {
41        let span = span.unwrap_or(Span::call_site());
42        Ident::new(self.keyword, span)
43    }
44
45    #[doc(hidden)]
46    pub fn generate_macro_arm(&self, ty: &Telety) -> syn::Result<Option<TokenStream>> {
47        if let Some(implementation) = (self.generate_macro_tokens)(ty) {
48            let span = ty.item().span();
49
50            let ParameterIdents {
51                args,
52                needle,
53                haystack,
54            } = ParameterIdents::new(span);
55
56            let keyword = self.keyword(Some(span));
57            let version = self.version_lit(Some(span));
58            Ok(Some(quote_spanned! { span =>
59                (#version, #keyword $( ( $($#args:tt)* ) )?, $#needle:tt, $($#haystack:tt)*) => {
60                    #implementation
61                };
62            }))
63        } else {
64            Ok(None)
65        }
66    }
67
68    /// Creates a macro invocation to use this command with the telety-generated macro at `macro_path`.  
69    /// The output of the command will be inserted into `haystack` at each instance of `needle`.
70    /// `macro_path` must point to a valid telety-generated macro, otherwise a compile error will occur.  
71    /// To support future [Command]s, `args` are passed to the command invocation, but they are not currently used.  
72    /// ## Example
73    /// ```rust,ignore
74    /// # use syn::parse2;
75    /// #[proc_macro]
76    /// pub fn my_public_macro(tokens: TokenStream) -> TokenStream {
77    ///     // ...
78    ///     let my_needle: TokenTree = format_ident!("__my_needle__").into();
79    ///     v1::UNIQUE_IDENT.apply(
80    ///         &parse_quote!(crate::MyTeletyObj),
81    ///         &my_needle,
82    ///         quote! {
83    ///             my_crate::my_macro_implementation!(#my_needle);
84    ///         },
85    ///         None,
86    ///     )
87    /// }
88    /// #[doc(hidden)]
89    /// #[proc_macro]
90    /// pub fn my_macro_implementation(tokens: TokenStream) -> TokenStream {
91    ///     let ident: Ident = parse2(tokens);
92    ///     // ...
93    /// }
94    /// ```
95    pub fn apply(
96        &'static self,
97        macro_path: Path,
98        needle: impl Into<SingleToken>,
99        haystack: impl ToTokens,
100    ) -> Apply {
101        Apply::new(
102            self,
103            macro_path,
104            needle.into(),
105            haystack.into_token_stream(),
106        )
107    }
108
109    // pub fn apply(
110    //     &self,
111    //     args: Option<TokenStream>,
112    //     macro_path: &Path,
113    //     needle: impl Into<SingleToken>,
114    //     haystack: impl ToTokens,
115    // ) -> ItemMacro {
116    //     let needle = needle.into();
117
118    //     let span = haystack.span();
119    //     let version = self.version_lit(Some(span));
120    //     let keyword = self.keyword(Some(span));
121
122    //     let args = args.map(|ts| quote!((#ts)));
123
124    //     parse_quote_spanned! { span =>
125    //         #macro_path!(#version, #keyword #args, #needle, #haystack);
126    //     }
127    // }
128
129    // /// Similar to [Command::apply], except if `macro_path` does not include a macro, `fallback` will be expanded instead.
130    // /// (Standard macro limitations apply, such as not using `$` directly.)
131    // /// Note that macro_path must still be valid path of some sort (i.e. a type or value), otherwise compilation will fail.
132    // /// Additionally, for name resolution to succeed, `macro_path` must start with a qualifier (e.g. `::`, `self::`, `crate::`, ...).
133    // /// If you see the error "import resolution is stuck, try simplifying macro imports", you are probably missing the qualifier.
134    // /// Finally, the output will be placed inside a block, which means any items defined inside cannot be easily referenced elsewhere.
135    // /// The primary use-case is to create `impl`s.
136    // pub fn apply_or(
137    //     &self,
138    //     args: Option<TokenStream>,
139    //     macro_path: &Path,
140    //     needle: impl Into<SingleToken>,
141    //     haystack: impl ToTokens,
142    //     fallback: impl ToTokens,
143    //     telety_path: Option<&Path>,
144    // ) -> ItemMacro {
145    //     let needle = needle.into();
146
147    //     let span = haystack.span();
148    //     let version = self.version_lit(Some(span));
149    //     let keyword = self.keyword(Some(span));
150
151    //     let args = args.map(|ts| quote!((#ts)));
152
153    //     let telety_path = telety_path
154    //         .map(Cow::Borrowed)
155    //         .unwrap_or_else(|| Cow::Owned(parse_quote!(::telety)));
156
157    //     parse_quote_spanned! { span =>
158    //         #telety_path::util::try_invoke!(
159    //             #macro_path!(#version, #keyword #args, #needle, #haystack);
160    //             #fallback
161    //         );
162    //     }
163    // }
164
165    // /// Similar to [Command::apply_or], except `haystack` is expanded in the current module or block if `macro_path` is a macro,
166    // /// otherwise, `fallback` is expanded instead.
167    // /// `haystack` is forwarded through a `macro_rules!` macro, but `$` tokens within `haystack` will be
168    // /// automatically converted to work within the macro.
169    // /// `unique_macro_ident` must be an identifier unique to the crate, as the forwarding macro must be `#[macro_export]`.
170    // pub fn apply_exported_or(
171    //     &self,
172    //     args: Option<TokenStream>,
173    //     macro_path: &Path,
174    //     needle: impl Into<SingleToken>,
175    //     haystack: impl ToTokens,
176    //     fallback: impl ToTokens,
177    //     telety_path: Option<&Path>,
178    //     unique_macro_ident: &Ident,
179    // ) -> Vec<Item> {
180    //     let needle = needle.into();
181
182    //     let span = haystack.span();
183
184    //     let textual_macro_ident: Ident = format_ident!("my_macro_{unique_macro_ident}");
185
186    //     // Replace `$` in the original haystack with `$dollar dollar`
187    //     // because we have 2 extra layers of macro rules indirection
188    //     let haystack = crate::find_and_replace::find_and_replace(
189    //         Punct::new('$', Spacing::Alone),
190    //         quote!($dollar dollar),
191    //         haystack.into_token_stream(),
192    //     );
193
194    //     let haystack = quote_spanned! { span =>
195    //         // Export a macro...
196    //         #[doc(hidden)]
197    //         #[macro_export]
198    //         macro_rules! #unique_macro_ident {
199    //             ($dollar:tt) => {
200    //                 // which defines a macro, ...
201    //                 macro_rules! #textual_macro_ident {
202    //                     ($dollar dollar:tt) => {
203    //                         // which expands to `haystack`
204    //                         #haystack
205    //                     };
206    //                 }
207    //             };
208    //         }
209    //     };
210
211    //     let apply_or = Item::Macro(self.apply_or(
212    //         args,
213    //         macro_path,
214    //         needle,
215    //         haystack,
216    //         quote!(
217    //             #[doc(hidden)]
218    //             #[macro_export]
219    //             macro_rules! #unique_macro_ident {
220    //                 ($dollar:tt) => {
221    //                     // which defines a macro, ...
222    //                     macro_rules! #textual_macro_ident {
223    //                         ($dollar dollar:tt) => {
224    //                             // which expands to `fallback`
225    //                             #fallback
226    //                         };
227    //                     }
228    //                 };
229    //             }
230    //         ),
231    //         telety_path,
232    //     ));
233
234    //     let temp_ident = format_ident!("_{unique_macro_ident}");
235
236    //     // In order to invoke the macro from its export at the crate root, we need this trick:
237    //     // We must glob import the crate root, and invoke the macro without a path. To avoid polluting the current module,
238    //     // we make a sub-module, and invoke within there. Since we need to expand the caller's `haystack` in the main module,
239    //     // this macro is a layer of indirection which defines another macro. Name resolution does not like the main module
240    //     // peeking into our sub-module, so we will invoke our final macro using textual scope instead of module scope.
241    //     // #[macro_use] allows the macro to remain in textual scope after the sub-module, so we can invoke it there.
242    //     let import = parse_quote_spanned! { span =>
243    //         #[macro_use]
244    //         #[doc(hidden)]
245    //         mod #temp_ident {
246    //             pub(super) use crate::*;
247
248    //             #unique_macro_ident!($);
249    //         }
250    //     };
251
252    //     let invoke = parse_quote_spanned! { span =>
253    //         #textual_macro_ident!($);
254    //     };
255
256    //     vec![apply_or, import, invoke]
257    // }
258}
259
260pub(crate) struct ParameterIdents {
261    pub args: Ident,
262    pub needle: Ident,
263    pub haystack: Ident,
264}
265
266impl ParameterIdents {
267    pub fn new(span: Span) -> Self {
268        Self {
269            args: Ident::new("args", span),
270            needle: Ident::new("needle", span),
271            haystack: Ident::new("haystack", span),
272        }
273    }
274}
275
276/// Creates the [TokenStream] for the [Command] using the given arguments.  
277/// Can be interpolated directly in a [quote!] macro.
278pub struct Apply {
279    command: &'static Command,
280    macro_path: Path,
281    needle: SingleToken,
282    haystack: TokenStream,
283    args: Option<TokenStream>,
284    fallback: Option<TokenStream>,
285    telety_path: Option<Path>,
286    unique_macro_ident: Option<Ident>,
287}
288
289impl Apply {
290    fn new(
291        command: &'static Command,
292        macro_path: Path,
293        needle: SingleToken,
294        haystack: TokenStream,
295    ) -> Self {
296        Self {
297            command,
298            macro_path,
299            needle,
300            haystack,
301            args: None,
302            fallback: None,
303            telety_path: None,
304            unique_macro_ident: None,
305        }
306    }
307
308    #[doc(hidden)]
309    /// Pass arguments to the command invocation.  
310    /// Note: no commands currently use any arguments
311    pub fn with_arguments(mut self, arguments: impl ToTokens) -> Self {
312        self.args.replace(arguments.into_token_stream());
313        self
314    }
315
316    /// If `macro_path` does not contain a macro, instead expand to the `fallback` tokens.  
317    /// By default, the command or `fallback` will be expanded inside an anonymous block,
318    /// so any items cannot not be referenced from outside. Use [Apply::with_macro_forwarding]
319    /// to expand the output directly in this scope.
320    pub fn with_fallback(mut self, fallback: impl ToTokens) -> Self {
321        self.fallback.replace(fallback.into_token_stream());
322        self
323    }
324
325    /// Specify the location of the telety crate.  
326    /// This is only required if telety is not located at the default path `::telety`
327    /// and [Apply::with_fallback] is used.
328    pub fn with_telety_path(mut self, telety_path: Path) -> Self {
329        self.telety_path.replace(telety_path);
330        self
331    }
332
333    /// If a fallback is set, forward the final haystack/fallback tokens through a macro
334    /// so that they are evaluated without additional block scopes.  
335    /// This is usually required if you a creating a named item (such as a `struct` or `enum`), but
336    /// not for `impls`.
337    /// `unique_macro_ident` must be unique within the crate.  
338    /// This has no effect if [Apply::with_fallback] is not used.
339    pub fn with_macro_forwarding(mut self, unique_macro_ident: Ident) -> Self {
340        self.unique_macro_ident.replace(unique_macro_ident);
341        self
342    }
343}
344
345impl ToTokens for Apply {
346    fn to_tokens(&self, tokens: &mut TokenStream) {
347        let macro_path = &self.macro_path;
348        let needle = &self.needle;
349        let mut haystack = self.haystack.to_token_stream();
350        let args = self.args.as_ref().map(|ts| quote!((#ts)));
351
352        let span = self.haystack.span();
353        let version = self.command.version_lit(Some(span));
354        let keyword = self.command.keyword(Some(span));
355
356        let textual_macro_ident: Ident = format_ident!(
357            "my_macro_{}",
358            self.unique_macro_ident
359                .as_ref()
360                .unwrap_or(&format_ident!("a"))
361        );
362
363        let mut fallback = self.fallback.as_ref().map(|f| f.to_token_stream());
364
365        if let Some(fallback) = fallback.as_mut() {
366            if let Some(unique_macro_ident) = &self.unique_macro_ident {
367                let macro_wrapper = |contents: &TokenStream| {
368                    // Replace `$` in the original content with `$dollar dollar`
369                    // because we have 2 extra layers of macro rules indirection
370                    let contents = crate::find_and_replace::find_and_replace(
371                        Punct::new('$', Spacing::Alone),
372                        quote!($dollar dollar),
373                        contents.into_token_stream(),
374                    );
375
376                    quote_spanned! { span =>
377                        // Export a macro...
378                        #[doc(hidden)]
379                        #[macro_export]
380                        macro_rules! #unique_macro_ident {
381                            ($dollar:tt) => {
382                                // which defines a macro, ...
383                                macro_rules! #textual_macro_ident {
384                                    ($dollar dollar:tt) => {
385                                        // which expands to our actual contents
386                                        #contents
387                                    };
388                                }
389                            };
390                        }
391                    }
392                };
393
394                *fallback = macro_wrapper(fallback);
395                haystack = macro_wrapper(&haystack);
396            }
397        }
398
399        let mut output = parse_quote_spanned! { span =>
400            #macro_path!(#version, #keyword #args, #needle, #haystack);
401        };
402
403        if let Some(fallback) = fallback {
404            let telety_path = self
405                .telety_path
406                .as_ref()
407                .map(Cow::Borrowed)
408                .unwrap_or_else(|| Cow::Owned(parse_quote!(::telety)));
409
410            output = parse_quote_spanned! { span =>
411                #telety_path::util::try_invoke!(
412                    #output
413                    #fallback
414                );
415            };
416
417            if let Some(unique_macro_ident) = &self.unique_macro_ident {
418                let temp_ident = format_ident!("_{unique_macro_ident}");
419
420                // In order to invoke the macro from its export at the crate root, we need this trick:
421                // We must glob import the crate root, and invoke the macro without a path. To avoid polluting the current module,
422                // we make a sub-module, and invoke within there. Since we need to expand the caller's `haystack` in the main module,
423                // this macro is a layer of indirection which defines another macro. Name resolution does not like the main module
424                // peeking into our sub-module, so we will invoke our final macro using textual scope instead of module scope.
425                // #[macro_use] allows the macro to remain in textual scope after the sub-module, so we can invoke it there.
426                let import = quote_spanned! { span =>
427                    #[macro_use]
428                    #[doc(hidden)]
429                    mod #temp_ident {
430                        pub(super) use crate::*;
431
432                        #unique_macro_ident!($);
433                    }
434                };
435
436                let invoke = quote_spanned! { span =>
437                    #textual_macro_ident!($);
438                };
439
440                output = quote_spanned! { span =>
441                    #output
442                    #import
443                    #invoke
444                };
445            }
446        }
447
448        output.to_tokens(tokens);
449    }
450}