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}