format_like/lib.rs
1//! A macro for creating format-like macros
2//!
3//! Have you ever wanted to emulate the functionality of the `format!`
4//! family of macros, but with an output that is not a [`String`] or
5//! something built from a [`String`]?
6//!
7//! No?
8//!
9//! Well, still, this might still be interesting for you.
10//!
11//! `format-like` aims to let _you_ decide how to interpret what is
12//! inside `{}` pairs, instead of calling something like
13//! `std::fmt::Display::fmt(&value)`.
14//!
15//! Additionaly, it lets you create 3 other types of bracket pairs:
16//! `()`, `[]` and `<>`, so you can interpret things in even more
17//! ways! This does of course come with the regular escaping that the
18//! [`format!`] macro does, so `{{` is escaped to just `{`, the same
19//! being the case for the other delimiters as well.
20//!
21//! Here's how it works:
22//!
23//! ```rust
24//! # #![feature(decl_macro)]
25//! use format_like::format_like;
26//!
27//! struct CommentedString(String, Vec<(usize, String)>);
28//!
29//! let comment = "there is an error in this word";
30//! let text = "text";
31//! let range = 0..usize::MAX;
32//!
33//! let commented_string = format_like!(
34//! parse_str,
35//! [('{', parse_interpolation, false), ('<', parse_comment, true)],
36//! CommentedString(String::new(), Vec::new()),
37//! "This is <comment>regluar {}, interpolated and commented {range.end}",
38//! text
39//! );
40//! # macro parse_str($value:expr, $str:literal) {{ $value }}
41//! # macro parse_interpolation($value:expr, $modif:literal, $added:expr) {{ $value }}
42//! # macro parse_comment($value:expr, $modif:literal, $added:expr) {{ $value }}
43//! ```
44//!
45//! In this example, the `{}` should work as intended, but you also
46//! have access to `<>` interpolation. Inside `<>`, a comment will be
47//! added, with the associated `usize` being its position in the
48//! [`String`].
49//!
50//! This will all be done through the `parse_str`,
51//! `parse_interpolation` and `parse_comment` macros:
52//!
53//! ```rust
54//! #![feature(decl_macro)]
55//! macro parse_str($value:expr, $str:literal) {{
56//! let mut commented_string = $value;
57//! commented_string.0.push_str($str);
58//! commented_string
59//! }}
60//!
61//! macro parse_interpolation($value:expr, $modif:literal, $added:expr) {{
62//! let CommentedString(string, comments) = $value;
63//! let string = format!(concat!("{}{", $modif, "}"), string, $added);
64//! CommentedString(string, comments)
65//! }}
66//!
67//! macro parse_comment($value:expr, $_modif:literal, $added:expr) {{
68//! let mut commented_string = $value;
69//! commented_string
70//! .1
71//! .push((commented_string.0.len(), $added.to_string()));
72//! commented_string
73//! }}
74//! ```
75//!
76//! The `parse_str` macro will be responsible for handling the non
77//! `{}` or `<>` parts of the literal `&str`. The `parse_comment` and
78//! `parse_interpolation` methods will handle what's inside the `<>`
79//! and `{}` pairs, respectively.
80//!
81//! `parse_comment` and `parse_interpolation` must have three
82//! parameters, one for the `value` being modified (in this case, a
83//! `CommentedString`), one for the modifier (`"?", "#?", ".3", etc),
84//! which might come after a `":"` in the pair. and one for the object
85//! being added (it's [`Display`] objects in this case, but
86//! it could be anything else).
87//!
88//! Now, as I mentioned earlier, this crate is meant for you to create
89//! _your own_ format like macros, so you should package all of this
90//! up into a single macro, like this:
91//!
92//! ```rust
93//! #![feature(decl_macro)]
94//! use format_like::format_like;
95//!
96//! #[derive(Debug, PartialEq)]
97//! struct CommentedString(String, Vec<(usize, String)>);
98//!
99//! let comment = "there is an error in this word";
100//! let text = "text";
101//! let range = 0..usize::MAX;
102//!
103//! let commented_string = commented_string!(
104//! "This is <comment>regluar {}, interpolated and commented {range.end}",
105//! text
106//! );
107//!
108//! assert_eq!(
109//! commented_string,
110//! CommentedString(
111//! "This is regluar text, interpolated and commented 18446744073709551615".to_string(),
112//! vec![(8, "there is an error in this word".to_string())]
113//! )
114//! );
115//!
116//! macro commented_string($($parts:tt)*) {
117//! format_like!(
118//! parse_str,
119//! [('{', parse_interpolation, false), ('<', parse_comment, true)],
120//! CommentedString(String::new(), Vec::new()),
121//! $($parts)*
122//! )
123//! }
124//!
125//! macro parse_str($value:expr, $str:literal) {{
126//! let mut commented_string = $value;
127//! commented_string.0.push_str($str);
128//! commented_string
129//! }}
130//!
131//! macro parse_interpolation($value:expr, $modif:literal, $added:expr) {{
132//! let CommentedString(string, comments) = $value;
133//! let string = format!(concat!("{}{", $modif, "}"), string, $added);
134//! CommentedString(string, comments)
135//! }}
136//!
137//! macro parse_comment($value:expr, $_modif:literal, $added:expr) {{
138//! let mut commented_string = $value;
139//! commented_string.1.push((commented_string.0.len(), $added.to_string()));
140//! commented_string
141//! }}
142//! ```
143//!
144//! ## Forced inlining
145//!
146//! You might be wondering: What are the `false` and `true` in the
147//! second argument of [`format_like!`] used for?
148//!
149//! Well, they determine wether an argument _must_ be inlined (i.e. be
150//! placed within the string like `{arg}`). This is useful when you
151//! want to limit the types of arguments that a macro should handle.
152//!
153//! As you might have seen earlier, [`format_like!`] accepts member
154//! access, like `{range.end}`. If you force a parameter to always be
155//! placed inline, that limits the types of tokens your macro must be
156//! able to handle, so you could rewrite the `parse_comment` macro to
157//! be:
158//!
159//! ```rust
160//! #![feature(decl_macro)]
161//! macro parse_comment($value:expr, $modif:literal, $($identifier:ident).*) {{
162//! // innards
163//! }}
164//! ```
165//!
166//! While this may not seem useful, it comes with two interesting
167//! abilities:
168//!
169//! 1 - If arguments must be inlined, you are allowed to leave the
170//! pair empty, like `<>`, and you can handle this situation
171//! differently if you want.
172//! 2 - By accessing the `$identifiers` directly, you can manipulate
173//! them in whichever way you want, heck, they may not even point to
174//! any actual variable in the code, and could be some sort of
175//! differently handled string literal.
176//!
177//! ## Motivation
178//!
179//! Even after reading all that, I wouldn't be surprised if you
180//! haven't found any particular use for this crate, and that's fine.
181//!
182//! But here is what was _my_ motivation for creating it:
183//!
184//! In my _in development_ text editor [Duat], there _used to be_ a
185//! `text` macro, which created a `Text` struct, which was essentially
186//! a [`String`] with formatting `Tag`s added on to it.
187//!
188//! It used to work like this:
189//!
190//! ```rust,ignore
191//! let text = text!("start " [RedColor.subvalue] variable " " other_variable " ");
192//! ```
193//!
194//! This macro was a simple declarative macro, so while it was easy to
195//! implement, there were several drawbacks to its design:
196//!
197//! - It was ignored by rustfmt;
198//! - It didn't look like Rust;
199//! - tree-sitter failed at syntax highlighting it;
200//! - It didn't look like Rust;
201//! - Way too much space was occupied by simple things like `" "`;
202//! - It didn't look like Rust;
203//!
204//! And now I have replaced the old `text` macro with a new version,
205//! based on `format_like!`, which makes for a much cleaner design:
206//!
207//! ```rust,ignore
208//! let text = text!("start [RedColor.subvalue]{variable} {other_variable} ");
209//! ```
210//!
211//! [`Display`]: std::fmt::Display
212//! [Duat]: https://github.com/AhoyISki/duat
213use std::ops::Range;
214
215use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
216
217/// A macro for creating format-like macros
218///
219/// ```rust
220/// #![feature(decl_macro)]
221/// use format_like::format_like;
222///
223/// #[derive(Debug, PartialEq)]
224/// struct CommentedString(String, Vec<(usize, String)>);
225///
226/// let comment = "there is an error in this word";
227/// let text = "text";
228/// let range = 0..usize::MAX;
229///
230/// let commented_string = commented_string!(
231/// "This is <comment>worng {}, and this is the end of the range {range.end}",
232/// text
233/// );
234///
235/// assert_eq!(
236/// commented_string,
237/// CommentedString(
238/// "This is worng text, and this is the end of the range 18446744073709551615".to_string(),
239/// vec![(8, "there is an error in this word".to_string())]
240/// )
241/// );
242///
243/// macro commented_string($($parts:tt)*) {
244/// format_like!(
245/// parse_str,
246/// [('{', parse_interpolation, false), ('<', parse_comment, true)],
247/// CommentedString(String::new(), Vec::new()),
248/// $($parts)*
249/// )
250/// }
251///
252/// macro parse_str($value:expr, $str:literal) {{
253/// let mut commented_string = $value;
254/// commented_string.0.push_str($str);
255/// commented_string
256/// }}
257///
258/// macro parse_interpolation($value:expr, $modif:literal, $added:expr) {{
259/// let CommentedString(string, comments) = $value;
260/// let string = format!(concat!("{}{", $modif, "}"), string, $added);
261/// CommentedString(string, comments)
262/// }}
263///
264/// macro parse_comment($value:expr, $_modif:literal, $added:expr) {{
265/// let mut commented_string = $value;
266/// commented_string.1.push((commented_string.0.len(), $added.to_string()));
267/// commented_string
268/// }}
269/// ```
270#[proc_macro]
271pub fn format_like(input: TokenStream) -> TokenStream {
272 let fmt_like = match FormatLike::parse(input.into_iter()) {
273 Ok(fmt_like) => fmt_like,
274 Err(err) => return err,
275 };
276
277 let str = fmt_like.str;
278 let arg_parsers = &fmt_like.arg_parsers;
279
280 let mut args = Vec::new();
281
282 let mut arg: Option<CurrentArg> = None;
283 let mut unescaped_rhs: Option<(usize, char)> = None;
284 let mut push_new_ident = true;
285 let mut positional_needed = 0;
286
287 let str_span = |_r: Range<usize>| fmt_like.str_lit.span();
288
289 for (i, char) in str.char_indices() {
290 if let Some((j, p, mut idents, mut modif)) = arg.take() {
291 let [lhs, rhs] = arg_parsers[p].delims;
292 if char == rhs {
293 let modif = match modif {
294 Some(range) => {
295 let str =
296 unsafe { str::from_utf8_unchecked(&str.as_bytes()[range.clone()]) };
297 let mut str = Literal::string(str);
298 str.set_span(fmt_like.str_lit.span());
299
300 TokenStream::from(TokenTree::Literal(str))
301 }
302 None => TokenStream::from(TokenTree::Literal(Literal::string(""))),
303 };
304
305 if idents.is_empty() {
306 if arg_parsers[p].inline_only {
307 args.push(Arg::Inlined(p, TokenStream::new(), modif));
308 } else {
309 positional_needed += 1;
310 args.push(Arg::Positional(p, j..i + 1, modif));
311 }
312 } else if push_new_ident {
313 return compile_err(
314 str_span(i - 1..i),
315 "invalid format string: field access expected an identifier",
316 );
317 } else {
318 let mut stream = Vec::new();
319 let len = idents.len();
320
321 for (i, (range, is_tuple_member)) in idents.into_iter().enumerate() {
322 let str =
323 unsafe { str::from_utf8_unchecked(&str.as_bytes()[range.clone()]) };
324
325 stream.push(if is_tuple_member {
326 TokenTree::Literal({
327 let mut literal = Literal::usize_unsuffixed(str.parse().unwrap());
328 literal.set_span(str_span(range.clone()));
329 literal
330 })
331 } else {
332 TokenTree::Ident(Ident::new(str, str_span(range.clone())))
333 });
334
335 if i != len - 1 {
336 stream.push(TokenTree::Punct({
337 let mut punct = Punct::new('.', Spacing::Alone);
338 punct.set_span(str_span(range.end..range.end + 1));
339 punct
340 }));
341 }
342 }
343
344 args.push(Arg::Inlined(p, TokenStream::from_iter(stream), modif));
345 }
346
347 continue;
348 } else if char == lhs && idents.is_empty() {
349 // If arg was empty, that means the delimiter was repeated, so escape
350 // it.
351 extend_str_arg(&mut args, char, i - 1);
352 continue;
353 }
354
355 // We might have mismatched delimiters
356 if arg_parsers
357 .iter()
358 .any(|ap| char == ap.delims[0] || char == ap.delims[1])
359 {
360 return TokenStream::from_iter([
361 compile_err(
362 str_span(i..i + 1),
363 "invalid format string: wrong match for delimiter",
364 ),
365 compile_err(
366 str_span(j..j + 1),
367 format!("from this delimiter, expected {rhs}"),
368 ),
369 ]);
370 } else if char.is_alphanumeric() || char == '_' || modif.is_some() {
371 if let Some(modif) = &mut modif {
372 modif.end = i + 1;
373 } else if let Some((range, is_tuple_member)) = idents.last_mut()
374 && !push_new_ident
375 {
376 *is_tuple_member &= char.is_ascii_digit();
377 range.end = i + 1;
378 } else {
379 idents.push((i..i + 1, char.is_ascii_digit()));
380 push_new_ident = false;
381 }
382 } else if char == '.' {
383 if let Some(modif) = &mut modif {
384 modif.end = i + 1;
385 } else if push_new_ident {
386 // Can't start an identifier list with '.' or put multiple '.'s in a
387 // row.
388 return compile_err(
389 str_span(i..i + 1),
390 "invalid format string: unexpected '.' here",
391 );
392 } else {
393 push_new_ident = true;
394 }
395 } else if char == ':' {
396 if let Some(modif) = &mut modif {
397 modif.end = i + 1;
398 } else {
399 modif = Some(i + 1..i + 1);
400 }
401 } else {
402 return compile_err(
403 str_span(i..i + 1),
404 format!("invalid format string: unexpected {char} here"),
405 );
406 }
407
408 arg = Some((j, p, idents, modif));
409 } else if let Some(p) = arg_parsers
410 .iter()
411 .position(|ap| char == ap.delims[0] || char == ap.delims[1])
412 {
413 // If the char is a left delimiter, begin an argument.
414 // If it is a right delimiter, handle dangling right parameter
415 // scenarios.
416 if char == arg_parsers[p].delims[0] {
417 push_new_ident = true;
418 arg = Some((i, p, Vec::new(), None));
419 } else if let Some((j, unescaped)) = unescaped_rhs {
420 // Double delimiters are escaped.
421 if char == unescaped {
422 unescaped_rhs = None;
423 extend_str_arg(&mut args, char, i);
424 } else {
425 return compile_err(
426 str_span(j..j + 1),
427 format!("invalid format string: unmatched {unescaped} found"),
428 );
429 }
430 } else {
431 unescaped_rhs = Some((i, char));
432 }
433 } else if let Some((j, unescaped)) = unescaped_rhs {
434 return compile_err(
435 str_span(j..j + 1),
436 format!("invalid format string: unmatched {unescaped} found"),
437 );
438 } else {
439 extend_str_arg(&mut args, char, i);
440 }
441 }
442
443 if let Some((i, unescaped)) = unescaped_rhs {
444 return compile_err(
445 str_span(i..i + 1),
446 format!("invalid format string: unmatched {unescaped} found"),
447 );
448 }
449
450 let mut token_stream = fmt_like.initial;
451
452 let positional_provided = fmt_like.exprs.len();
453 let mut exprs = fmt_like.exprs.into_iter();
454
455 let comma = TokenTree::Punct(Punct::new(',', Spacing::Alone));
456
457 for arg in args {
458 token_stream = match arg {
459 Arg::Str(string, range) => {
460 let mut str = Literal::string(&string);
461 str.set_span(str_span(range));
462
463 recurse_parser(
464 &fmt_like.str_parser,
465 token_stream
466 .into_iter()
467 .chain([comma.clone(), TokenTree::Literal(str)]),
468 )
469 }
470 Arg::Positional(p, range, modif) => {
471 if let Some(expr) = exprs.next() {
472 recurse_parser(
473 &fmt_like.arg_parsers[p].parser,
474 token_stream
475 .into_iter()
476 .chain([comma.clone()])
477 .chain(modif)
478 .chain([comma.clone()])
479 .chain(expr),
480 )
481 } else {
482 let npl = if positional_needed == 1 { "" } else { "s" };
483 let pverb = if positional_provided == 1 {
484 "is"
485 } else {
486 "are"
487 };
488 let ppl = if positional_provided == 1 { "" } else { "s" };
489
490 return compile_err(
491 str_span(range),
492 format!(
493 "{positional_needed} positional argument{npl} in format string, but there {pverb} {positional_provided} argument{ppl}"
494 ),
495 );
496 }
497 }
498 Arg::Inlined(p, idents, modif) => recurse_parser(
499 &fmt_like.arg_parsers[p].parser,
500 token_stream
501 .into_iter()
502 .chain([comma.clone()])
503 .chain(modif)
504 .chain([comma.clone()])
505 .chain(idents),
506 ),
507 }
508 }
509
510 // There should be no positional arguments left.
511 if let Some(expr) = exprs.next() {
512 return compile_err(
513 expr.into_iter().next().unwrap().span(),
514 "argument never used",
515 );
516 }
517
518 token_stream
519}
520
521struct ArgParser {
522 delims: [char; 2],
523 delim_span: Span,
524 parser: Ident,
525 inline_only: bool,
526}
527
528struct FormatLike {
529 str_parser: Ident,
530 arg_parsers: Vec<ArgParser>,
531 initial: TokenStream,
532 str: String,
533 str_lit: Literal,
534 exprs: Vec<TokenStream>,
535}
536
537impl FormatLike {
538 fn parse(mut stream: impl Iterator<Item = TokenTree>) -> Result<Self, TokenStream> {
539 use TokenTree as TT;
540
541 let str_parser = get_ident(stream.next())?;
542
543 consume_comma(stream.next())?;
544
545 let arg_parsers = {
546 let group = match stream.next() {
547 Some(TT::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
548 Some(other) => return Err(compile_err(other.span(), "expected a list")),
549 _ => return Err(compile_err(Span::mixed_site(), "expected a list")),
550 };
551
552 let mut arg_parsers = Vec::new();
553
554 let mut stream = group.stream().into_iter();
555
556 loop {
557 static INVALID_ERR: &str = "expected one of '{', '(', '[', or '<'";
558
559 let group = match stream.next() {
560 Some(TT::Group(group)) if group.delimiter() == Delimiter::Parenthesis => group,
561 None => break,
562 Some(other) => return Err(compile_err(other.span(), "expected a tuple")),
563 };
564
565 let mut substream = group.stream().into_iter();
566
567 let (delims, delim_span) = match substream.next() {
568 Some(TT::Literal(literal)) => match literal.to_string().as_str() {
569 "'{'" => (['{', '}'], literal.span()),
570 "'('" => (['(', ')'], literal.span()),
571 "'['" => (['[', ']'], literal.span()),
572 "'<'" => (['<', '>'], literal.span()),
573 _ => return Err(compile_err(literal.span(), INVALID_ERR)),
574 },
575 Some(other) => return Err(compile_err(other.span(), INVALID_ERR)),
576 _ => return Err(compile_err(Span::mixed_site(), INVALID_ERR)),
577 };
578
579 consume_comma(substream.next())?;
580
581 let parser = get_ident(substream.next())?;
582
583 consume_comma(substream.next())?;
584
585 let inline_only = match substream.next() {
586 Some(TT::Ident(ident)) => match ident.to_string().as_str() {
587 "true" => true,
588 "false" => false,
589 _ => {
590 return Err(compile_err(
591 ident.span(),
592 format!("expected a bool, got {ident:?}"),
593 ));
594 }
595 },
596 Some(other) => {
597 return Err(compile_err(
598 other.span(),
599 format!("expected a bool, got {other}"),
600 ));
601 }
602 _ => return Err(compile_err(Span::mixed_site(), "expected a bool")),
603 };
604
605 arg_parsers.push(ArgParser { delims, delim_span, parser, inline_only });
606
607 _ = consume_comma(stream.next());
608 }
609
610 arg_parsers
611 };
612
613 if let Some((lhs, rhs)) = arg_parsers.iter().enumerate().find_map(|(i, lhs)| {
614 arg_parsers.iter().enumerate().find_map(|(j, rhs)| {
615 (i != j)
616 .then(|| (rhs.delims == lhs.delims).then_some((lhs, rhs)))
617 .flatten()
618 })
619 }) {
620 return Err(TokenStream::from_iter([
621 compile_err(lhs.delim_span, "this delimiter"),
622 compile_err(rhs.delim_span, "is the same as this"),
623 ]));
624 }
625
626 consume_comma(stream.next())?;
627
628 let initial = {
629 let mut initial = Vec::new();
630
631 for token in stream.by_ref() {
632 if let TokenTree::Punct(punct) = &token
633 && punct.as_char() == ','
634 {
635 break;
636 }
637
638 initial.push(token);
639 }
640
641 TokenStream::from_iter(initial)
642 };
643
644 let (str, str_lit) = match stream.next() {
645 Some(TokenTree::Literal(literal)) => {
646 let mut str = literal.to_string();
647 if str.starts_with('"') && str.ends_with('"') {
648 str.pop();
649 str.remove(0);
650 (str, literal)
651 } else {
652 return Err(compile_err(literal.span(), "expected a string literal"));
653 }
654 }
655 Some(other) => return Err(compile_err(other.span(), "expected a string literal")),
656 None => return Err(compile_err(Span::mixed_site(), "expected a string literal")),
657 };
658
659 let exprs = match stream.next() {
660 Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => {
661 let mut exprs = Vec::new();
662
663 let mut tokens = Vec::new();
664
665 for token in stream {
666 if let TokenTree::Punct(punct) = &token
667 && punct.as_char() == ','
668 {
669 if !tokens.is_empty() {
670 exprs.push(TokenStream::from_iter(tokens.drain(..)));
671 }
672 } else {
673 tokens.push(token);
674 }
675 }
676
677 if !tokens.is_empty() {
678 exprs.push(TokenStream::from_iter(tokens));
679 }
680
681 exprs
682 }
683 Some(other) => return Err(compile_err(other.span(), "expected a comma")),
684 None => Vec::new(),
685 };
686
687 Ok(Self {
688 str_parser,
689 arg_parsers,
690 initial,
691 str,
692 str_lit,
693 exprs,
694 })
695 }
696}
697
698enum Arg {
699 Str(String, Range<usize>),
700 Positional(usize, Range<usize>, TokenStream),
701 Inlined(usize, TokenStream, TokenStream),
702}
703
704fn recurse_parser(parser: &Ident, stream: impl Iterator<Item = TokenTree>) -> TokenStream {
705 let start = parser.span().start();
706
707 TokenStream::from_iter([
708 TokenTree::Ident(parser.clone()),
709 TokenTree::Punct({
710 let mut punct = Punct::new('!', Spacing::Alone);
711 punct.set_span(start);
712 punct
713 }),
714 TokenTree::Group(Group::new(Delimiter::Parenthesis, stream.collect())),
715 ])
716}
717
718fn consume_comma(value: Option<TokenTree>) -> Result<(), TokenStream> {
719 match value {
720 Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => Ok(()),
721 Some(other) => Err(compile_err(other.span(), "Expected a comma")),
722 _ => Err(compile_err(Span::mixed_site(), "Expected a comma")),
723 }
724}
725
726fn get_ident(value: Option<TokenTree>) -> Result<Ident, TokenStream> {
727 match value {
728 Some(TokenTree::Ident(ident)) => Ok(ident),
729 Some(other) => Err(compile_err(other.span(), "Expected an identifier")),
730 _ => Err(compile_err(Span::mixed_site(), "Expected an identifier")),
731 }
732}
733
734fn extend_str_arg(args: &mut Vec<Arg>, char: char, i: usize) {
735 if let Some(Arg::Str(string, range)) = args.last_mut() {
736 string.push(char);
737 range.end = i + 1;
738 } else {
739 args.push(Arg::Str(String::from(char), i..i + 1))
740 }
741}
742
743fn compile_err(span: Span, msg: impl std::fmt::Display) -> TokenStream {
744 let (start, end) = (span.start(), span.end());
745
746 TokenStream::from_iter([
747 TokenTree::Punct({
748 let mut punct = Punct::new(':', Spacing::Joint);
749 punct.set_span(start);
750 punct
751 }),
752 TokenTree::Punct({
753 let mut punct = Punct::new(':', Spacing::Alone);
754 punct.set_span(start);
755 punct
756 }),
757 TokenTree::Ident(Ident::new("core", start)),
758 TokenTree::Punct({
759 let mut punct = Punct::new(':', Spacing::Joint);
760 punct.set_span(start);
761 punct
762 }),
763 TokenTree::Punct({
764 let mut punct = Punct::new(':', Spacing::Alone);
765 punct.set_span(start);
766 punct
767 }),
768 TokenTree::Ident({
769 let mut ident = Ident::new("compile_error", start);
770 ident.set_span(start);
771 ident
772 }),
773 TokenTree::Punct({
774 let mut punct = Punct::new('!', Spacing::Alone);
775 punct.set_span(start);
776 punct
777 }),
778 TokenTree::Group({
779 let mut group = Group::new(Delimiter::Brace, {
780 TokenStream::from_iter([TokenTree::Literal({
781 let mut string = Literal::string(&msg.to_string());
782 string.set_span(end);
783 string
784 })])
785 });
786 group.set_span(end);
787 group
788 }),
789 ])
790}
791
792type CurrentArg = (
793 usize,
794 usize,
795 Vec<(Range<usize>, bool)>,
796 Option<Range<usize>>,
797);