Skip to main content

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