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})}