unstringify/
lib.rs

1//! # `unstringify!`
2//!
3//! See [the documentation of the macro for more info][`unstringify!`]
4
5/// For compat with older versions of `rustc`.
6extern crate proc_macro;
7
8use ::proc_macro::{*,
9    TokenTree as TT,
10};
11
12#[macro_use]
13mod utils;
14
15struct Input {
16    tokenized: TokenStream,
17    metavar: Ident,
18    template: TokenStream,
19}
20
21/// Reverse of [`stringify!`]: tokenize an input string literal.
22///
23/// ## Basic example
24///
25/// ```rust
26/// use ::unstringify::unstringify;
27///
28/// unstringify!(r#"
29///     fn main ()
30///     {
31///         println!("Hello, World!");
32///     }
33/// "#);
34/// ```
35///
36/// Or, equivalently:
37///
38/// ```rust
39/// use ::unstringify::unstringify;
40///
41/// unstringify!(stringify! {
42///     fn main ()
43///     {
44///         println!("Hello, World!");
45///     }
46/// });
47/// ```
48///
49/// <details><summary>▶ A more interesting example</summary>
50///
51/// A (non-procedural!) macro to evaluate the rust code snippets inside
52/// docstrings:
53///
54/// ```rust,should_panic
55/// use ::unstringify::unstringify;
56///
57/// macro_rules! eval_docstrings {(
58///     $(
59///         #[doc = $doc:tt]
60///     )*
61/// ) => (
62///     extract_code_snippets! {
63///         @find_start
64///         $($doc)*
65///     }
66/// )}
67///
68/// macro_rules! extract_code_snippets {
69///     (
70///         @find_start
71///         r" ```rust"
72///         $($rest:tt)*
73///     ) => (
74///         extract_code_snippets! {
75///             @accumulate_into []
76///             $($rest)*
77///         }
78///     );
79///
80///     (
81///         @find_start
82///         $otherwise_ignored:tt // ≠ " ```rust"
83///         $($rest:tt)*
84///     ) => (
85///         extract_code_snippets! {
86///             @find_start
87///             $($rest)*
88///         }
89///     );
90///
91///     (
92///         @find_start
93///         // No lines left
94///     ) => (
95///         // The end.
96///     );
97///
98///     // End of code snippet found,
99///     // TIME TO EVALUATE THE CODE!
100///     (
101///         @accumulate_into [ $($lines:tt)* ]
102///         r" ```"
103///         $($rest:tt)*
104///     ) => (
105///         // evaluate the code...
106///         unstringify!(concat!(
107///             $($lines),*
108///         ));
109///         // ... and rince and repeat with the remaining docstrings
110///         extract_code_snippets! {
111///             @find_start
112///             $($rest)*
113///         }
114///     );
115///
116///     // Basic recursion step: accumulate a non-terminating line
117///     (
118///         @accumulate_into [ $($lines:tt)* ]
119///         $current_line:tt // ≠ " ```"
120///         $($rest:tt)*
121///     ) => (
122///         extract_code_snippets! {
123///             @accumulate_into [ $($lines)* $current_line ]
124///             $($rest)*
125///         }
126///     );
127/// }
128///
129/// eval_docstrings! {
130///     /// This is a comment.
131///     /// As ordinary as they make them.
132///     ///
133///     /// And yet...
134///     /// Sometimes...
135///     ///
136///     /// > A code snippet appears!
137///     ///
138///     /// ```rust
139///     /// panic!("Successfully managed to evaluate this panic (and thus panic)");
140///     /// ```
141///     ///
142///     /// Impressive, ain't it?
143/// }
144/// ```
145///
146/// ___
147///
148/// </details>
149///
150/// ## Remarks
151///
152/// This intuitive API very quickly encounters limitations, related not the
153/// macro itself, but rather to the way Rust expands macros.
154///
155/// So, for instance, the following assertion fails:
156///
157/// ```rust,should_panic
158/// # use ::unstringify::unstringify;
159/// #
160/// assert_eq!(
161///     stringify!(unstringify!("example")),
162///     "example",
163/// );
164/// ```
165///
166/// Indeed, in the above code the macro `stringify!` is called _before_
167/// `unstringify!`, so what happens is `stringify!` simply stringifies its input
168/// tokens, _verbatim_, without evaluating them: `'unstringify!("example")'`. 🤦
169///
170/// To solve that, [`unstringify!`] features "preprocessor" capabilities
171/// similar to [`::paste::paste!`](https://docs.rs/paste), that allow to
172/// circumvent this limitation, by doing:
173///
174/// ```rust
175/// # use ::unstringify::unstringify;
176/// #
177/// assert_eq!(
178///     unstringify!(let $tokens = unstringify!("example") in {
179///         stringify!($tokens)
180///     }),
181///     "example",
182/// );
183/// ```
184///
185/// ___
186///
187/// Also, for the same reason but reversed this time, the input fed to
188/// [`unstringify!`] cannot be eagerly macro-expanded.
189///
190/// This means that the following fails:
191///
192/// ```rust,compile_fail
193/// # use ::unstringify::unstringify;
194/// #
195/// macro_rules! my_macro {() => ("fn main () {}")}
196///
197/// unstringify!(my_macro!());
198/// ```
199///
200///   - The workaround is to define things such as `my_macro!` using, for
201///     instance, the callback pattern:
202///
203///     ```rust
204///     # use ::unstringify::unstringify;
205///     #
206///     macro_rules! my_macro {(
207///         => $callback:ident !
208///     ) => (
209///         $callback! { "fn main () {}" }
210///     )}
211///
212///     my_macro!(=> unstringify!);
213///     ```
214///
215/// That being said, the astute reader may retort:
216///
217/// > But wait, doesn't your second example within this documentation showcase
218/// > `unstringify!(stringify! { ...  })`?
219///
220/// And indeed it does. This is achieved by hard-coding the (basic) logic of
221/// `stringify!` and `concat!` inside the [`unstringify!`] macro (for instance,
222/// when [`unstringify!`] stumbles upon a `stringify! { ... }` (which is _not_, I
223/// repeat, a _verbatim_ string literal), it decides to simply emit the inner
224/// `...`).
225#[proc_macro] pub
226fn unstringify (input: TokenStream)
227  -> TokenStream
228{
229    match tokenize_string_literal_or_concat_or_stringify(
230        input.clone().into_iter().peekable()
231    )
232    {
233        | Ok((tokenized, mut remaining)) => if remaining.next().is_none() {
234            return tokenized;
235        },
236        | _ => {}
237    }
238    let Input {
239        tokenized, metavar, template,
240    } = match let_unstringify(input) {
241        | Ok(it) => it,
242        | Err((span, err_msg)) => {
243            macro_rules! spanned {($expr:expr) => (
244                match $expr { mut expr => {
245                    expr.set_span(span);
246                    expr
247                }}
248            )}
249            return ts![
250                Ident::new("compile_error", span),
251                spanned!(Punct::new('!', Spacing::Alone)),
252                spanned!(ts![ (
253                    Literal::string(&*err_msg),
254                )]),
255                spanned!(Punct::new(';', Spacing::Alone)),
256            ];
257        },
258    };
259    map_replace(&metavar.to_string(), &tokenized, template)
260}
261
262/// `let $var = unstringify!("...") in { ... }`
263fn let_unstringify (input: TokenStream)
264  -> Result<
265        Input,
266        (Span, ::std::borrow::Cow<'static, str>),
267    >
268{
269    let mut tokens = input.into_iter().peekable();
270    unwrap_next_token! {
271        if let TT::Ident(ident) = tokens.next(),
272            if (ident.to_string() == "let")
273        {} else {
274            failwith!("expected `let`");
275        }
276    }
277    unwrap_next_token! {
278        if let TT::Punct(p) = tokens.next(),
279            if (p.as_char() == '$')
280        {} else {
281            failwith!("expected `$`");
282        }
283    }
284    let metavar = unwrap_next_token! {
285        if let TT::Ident(it) = tokens.next(), { it } else {
286            failwith!("expected an identifier");
287        }
288    };
289    unwrap_next_token! {
290        if let TT::Punct(p) = tokens.next(),
291            if (p.as_char() == '=')
292        {} else {
293            failwith!("expected `=`");
294        }
295    }
296    unwrap_next_token! {
297        if let TT::Ident(ident) = tokens.next(),
298            if (ident.to_string() == "unstringify")
299        {} else {
300            failwith!("expected `unstringify`");
301        }
302    }
303    unwrap_next_token! {
304        if let TT::Punct(p) = tokens.next(),
305            if (p.as_char() == '!')
306        {} else {
307            failwith!("expected `!`");
308        }
309    }
310    let tokenized: TokenStream = {
311        let tokenize_args = unwrap_next_token! {
312            if let TT::Group(group) = tokens.next(),
313                if (matches!(group.delimiter(), Delimiter::Parenthesis))
314            {
315                group.stream().into_iter()
316            } else {
317                failwith!("expected `( ... )`");
318            }
319        };
320        let (tokenized, mut remaining) =
321            tokenize_string_literal_or_concat_or_stringify(
322                tokenize_args.into_iter().peekable(),
323            )?
324        ;
325        if let Some(extraneous_tt) = remaining.next() {
326            return Err((
327                extraneous_tt.span(),
328                "extraneous token(s)".into(),
329            ));
330        }
331        tokenized
332    };
333    unwrap_next_token! {
334        if let TT::Ident(in_) = tokens.next(),
335            if (in_.to_string() == "in")
336        {} else {
337            failwith!("expected `;`");
338        }
339    }
340    let rest = unwrap_next_token! {
341        if let TT::Group(group) = tokens.next(),
342        {
343            group.stream()
344        } else {
345            failwith!("expected `{ ... }` or `( ... )` or `[ ... ]`");
346        }
347    };
348    if let Some(extraneous_tt) = tokens.next() {
349        return Err((
350            extraneous_tt.span(),
351            "extraneous token(s)".into(),
352        ));
353    }
354    Ok(Input {
355        tokenized,
356        metavar,
357        template: rest,
358    })
359}
360
361fn map_replace (
362    metavar: &'_ String,
363    tokenized: &'_ TokenStream,
364    tokens: TokenStream
365) -> TokenStream
366{
367    let mut tokens = tokens.into_iter().peekable();
368    let mut ret = TokenStream::new();
369    loop {
370        match (tokens.next(), tokens.peek()) {
371            | (
372                Some(TT::Punct(dollar)),
373                Some(TT::Ident(ident)),
374            )
375                if  dollar.as_char() == '$'
376                &&  ident.to_string() == *metavar
377            => {
378                drop(tokens.next());
379                ret.extend(tokenized.clone());
380            },
381
382            | (Some(TT::Group(group)), _) => {
383                ret.extend(Some(TT::Group(Group::new(
384                    group.delimiter(),
385                    map_replace(metavar, tokenized, group.stream()),
386                ))));
387            },
388
389            | (None, _) => break,
390
391            | (tt, _) => ret.extend(tt),
392        }
393    }
394    ret
395}
396
397type Tokens = ::core::iter::Peekable<token_stream::IntoIter>;
398
399/// Input may be a:
400///
401///   - a string literal (terminal);
402///
403///   - a `stringify! { ... }` call (verbatim);
404///
405///   - a `concat!(...)` call whose args can be any of these three options:
406///     recurse.
407///
408/// To recurse, especially when wanting to parse a comma-separated sequence of
409/// expressions, we return not only the successfully parsed-and-then-tokenized
410/// input, we also return the trailing tokens, to keep iterating the logic
411/// if they start with a `,`.
412fn tokenize_string_literal_or_concat_or_stringify (
413    mut tokens: Tokens,
414) -> Result<
415        (TokenStream, Tokens),
416        (Span, ::std::borrow::Cow<'static, str>),
417    >
418{Ok({
419    let recurse = tokenize_string_literal_or_concat_or_stringify;
420    macro_rules! err_msg {() => (
421        "expected \
422            a string literal, \
423            a verbatim `stringify!` call, \
424            or a verbatim `concat!` call.\
425        "
426    )}
427    let mut s: String;
428    let ret = match tokens.next() {
429        // Un-group the `$var` metavariables.
430        | Some(TT::Group(group))
431            if matches!(group.delimiter(), Delimiter::None)
432        => {
433            let mut flattened = group.stream();
434            flattened.extend(tokens);
435            return recurse(flattened.into_iter().peekable());
436        },
437
438        | Some(TT::Literal(lit))
439            if {
440                s = lit.to_string();
441                utils::extracted_string_literal(&mut s)
442            }
443        => match s.parse::<TokenStream>() {
444            | Ok(ts) => ts,
445            | Err(err) => return Err((
446                lit.span(),
447                format!("Invalid tokens: {}", err).into(),
448            )),
449        },
450
451        | Some(TT::Ident(ident))
452            if matches!(
453                tokens.peek(),
454                Some(TT::Punct(p)) if p.as_char() == '!'
455            )
456        => {
457            drop(tokens.next());
458            let group_contents = unwrap_next_token! {
459                if let TT::Group(group) = tokens.next(), {
460                    group.stream()
461                } else {
462                    failwith!("\
463                        expected `{ ... }` or `( ... )` or `[ ... ]`\
464                    ");
465                }
466            };
467            match ident.to_string().as_str() {
468                | "stringify" => group_contents,
469                | "concat" => {
470                    let mut ret = TokenStream::new();
471                    let mut current = group_contents.into_iter().peekable();
472                    loop {
473                        let (parsed, mut remaining) = recurse(current)?;
474                        ret.extend(parsed);
475                        if remaining.peek().is_none() {
476                            break ret;
477                        }
478                        unwrap_next_token! {
479                            if let TT::Punct(p) = remaining.next(),
480                                if (p.as_char() == ',')
481                            {} else {
482                                failwith!("expected nothing or `,`");
483                            }
484                        }
485                        if remaining.peek().is_none() {
486                            break ret;
487                        }
488                        current = remaining;
489                    }
490                },
491                | _ => return Err((
492                    ident.span(),
493                    "expected `stringify` or `concat`".into(),
494                )),
495            }
496        },
497
498        | Some(bad_tt) => return Err((
499            bad_tt.span(),
500            err_msg!().into(),
501        )),
502
503        | None => return Err((
504            Span::call_site(),
505            concat!("Unexpected end of input: ", err_msg!()).into(),
506        )),
507    };
508    (ret, tokens)
509})}