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 litrs::StringLit;
216use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
217
218/// A macro for creating format-like macros
219///
220/// ```rust
221/// #![feature(decl_macro)]
222/// use format_like::format_like;
223///
224/// #[derive(Debug, PartialEq)]
225/// struct CommentedString(String, Vec<(usize, String)>);
226///
227/// let comment = "there is an error in this word";
228/// let text = "text";
229/// let range = 0..usize::MAX;
230///
231/// let commented_string = commented_string!(
232///     "This is <comment>worng {}, and this is the end of the range {range.end}",
233///     text
234/// );
235///
236/// assert_eq!(
237///     commented_string,
238///     CommentedString(
239///         "This is worng text, and this is the end of the range 18446744073709551615".to_string(),
240///         vec![(8, "there is an error in this word".to_string())]
241///     )
242/// );
243///
244/// macro commented_string($($parts:tt)*) {
245///     format_like!(
246///         parse_str,
247///         [('{', parse_interpolation, false), ('<', parse_comment, true)],
248///         CommentedString(String::new(), Vec::new()),
249///         $($parts)*
250///     )
251/// }
252///
253/// macro parse_str($value:expr, $str:literal) {{
254///     let mut commented_string = $value;
255///     commented_string.0.push_str($str);
256///     commented_string
257/// }}
258///
259/// macro parse_interpolation($value:expr, $modif:literal, $added:expr) {{
260///     let CommentedString(string, comments) = $value;
261///     let string = format!(concat!("{}{", $modif, "}"), string, $added);
262///     CommentedString(string, comments)
263/// }}
264///
265/// macro parse_comment($value:expr, $_modif:literal, $added:expr) {{
266///     let mut commented_string = $value;
267///     commented_string.1.push((commented_string.0.len(), $added.to_string()));
268///     commented_string
269/// }}
270/// ```
271#[proc_macro]
272pub fn format_like(input: TokenStream) -> TokenStream {
273    let fmt_like = match FormatLike::parse(input.into_iter()) {
274        Ok(fmt_like) => fmt_like,
275        Err(err) => return err,
276    };
277
278    let str = fmt_like.str.value();
279    let arg_parsers = &fmt_like.arg_parsers;
280
281    let mut args = Vec::new();
282
283    let mut arg: Option<CurrentArg> = None;
284    let mut unescaped_rhs: Option<(usize, char)> = None;
285    let mut push_new_ident = true;
286    let mut positional_needed = 0;
287
288    let str_span = |_r: Range<usize>| fmt_like.str_span;
289
290    for (i, char) in str.char_indices() {
291        if let Some((j, p, mut idents, mut modif)) = arg.take() {
292            let [lhs, rhs] = arg_parsers[p].delims;
293            if char == rhs {
294                let modif = match modif {
295                    Some(range) => {
296                        let str =
297                            unsafe { str::from_utf8_unchecked(&str.as_bytes()[range.clone()]) };
298                        let mut str = Literal::string(str);
299                        str.set_span(fmt_like.str_span);
300
301                        TokenStream::from(TokenTree::Literal(str))
302                    }
303                    None => TokenStream::from(TokenTree::Literal(Literal::string(""))),
304                };
305
306                if idents.is_empty() {
307                    if arg_parsers[p].inline_only {
308                        args.push(Arg::Inlined(p, TokenStream::new(), modif));
309                    } else {
310                        positional_needed += 1;
311                        args.push(Arg::Positional(p, j..i + 1, modif));
312                    }
313                } else if push_new_ident {
314                    return compile_err(
315                        str_span(i - 1..i),
316                        "invalid format string: field access expected an identifier",
317                    );
318                } else {
319                    let mut stream = Vec::new();
320                    let len = idents.len();
321
322                    for (i, (range, is_tuple_member)) in idents.into_iter().enumerate() {
323                        let str =
324                            unsafe { str::from_utf8_unchecked(&str.as_bytes()[range.clone()]) };
325
326                        stream.push(if is_tuple_member {
327                            TokenTree::Literal({
328                                let mut literal = Literal::usize_unsuffixed(str.parse().unwrap());
329                                literal.set_span(str_span(range.clone()));
330                                literal
331                            })
332                        } else {
333                            TokenTree::Ident(Ident::new(str, str_span(range.clone())))
334                        });
335
336                        if i != len - 1 {
337                            stream.push(TokenTree::Punct({
338                                let mut punct = Punct::new('.', Spacing::Alone);
339                                punct.set_span(str_span(range.end..range.end + 1));
340                                punct
341                            }));
342                        }
343                    }
344
345                    args.push(Arg::Inlined(p, TokenStream::from_iter(stream), modif));
346                }
347
348                continue;
349            } else if char == lhs && idents.is_empty() {
350                // If arg was empty, that means the delimiter was repeated, so escape
351                // it.
352                extend_str_arg(&mut args, char, i - 1);
353                continue;
354            }
355
356            // We might have mismatched delimiters
357            if arg_parsers
358                .iter()
359                .any(|ap| char == ap.delims[0] || char == ap.delims[1])
360            {
361                return TokenStream::from_iter([
362                    compile_err(
363                        str_span(i..i + 1),
364                        "invalid format string: wrong match for delimiter",
365                    ),
366                    compile_err(
367                        str_span(j..j + 1),
368                        format!("from this delimiter, expected {rhs}"),
369                    ),
370                ]);
371            } else if char.is_alphanumeric() || char == '_' || modif.is_some() {
372                if let Some(modif) = &mut modif {
373                    modif.end = i + 1;
374                } else if let Some((range, is_tuple_member)) = idents.last_mut()
375                    && !push_new_ident
376                {
377                    *is_tuple_member &= char.is_ascii_digit();
378                    range.end = i + 1;
379                } else {
380                    idents.push((i..i + 1, char.is_ascii_digit()));
381                    push_new_ident = false;
382                }
383            } else if char == '.' {
384                if let Some(modif) = &mut modif {
385                    modif.end = i + 1;
386                } else if push_new_ident {
387                    // Can't start an identifier list with '.' or put multiple '.'s in a
388                    // row.
389                    return compile_err(
390                        str_span(i..i + 1),
391                        "invalid format string: unexpected '.' here",
392                    );
393                } else {
394                    push_new_ident = true;
395                }
396            } else if char == ':' {
397                if let Some(modif) = &mut modif {
398                    modif.end = i + 1;
399                } else {
400                    modif = Some(i + 1..i + 1);
401                }
402            } else {
403                return compile_err(
404                    str_span(i..i + 1),
405                    format!("invalid format string: unexpected {char} here"),
406                );
407            }
408
409            arg = Some((j, p, idents, modif));
410        } else if let Some(p) = arg_parsers
411            .iter()
412            .position(|ap| char == ap.delims[0] || char == ap.delims[1])
413        {
414            // If the char is a left delimiter, begin an argument.
415            // If it is a right delimiter, handle dangling right parameter
416            // scenarios.
417            if char == arg_parsers[p].delims[0] {
418                push_new_ident = true;
419                arg = Some((i, p, Vec::new(), None));
420            } else if let Some((j, unescaped)) = unescaped_rhs {
421                // Double delimiters are escaped.
422                if char == unescaped {
423                    unescaped_rhs = None;
424                    extend_str_arg(&mut args, char, i);
425                } else {
426                    return compile_err(
427                        str_span(j..j + 1),
428                        format!("invalid format string: unmatched {unescaped} found"),
429                    );
430                }
431            } else {
432                unescaped_rhs = Some((i, char));
433            }
434        } else if let Some((j, unescaped)) = unescaped_rhs {
435            return compile_err(
436                str_span(j..j + 1),
437                format!("invalid format string: unmatched {unescaped} found"),
438            );
439        } else {
440            extend_str_arg(&mut args, char, i);
441        }
442    }
443
444    if let Some((i, unescaped)) = unescaped_rhs {
445        return compile_err(
446            str_span(i..i + 1),
447            format!("invalid format string: unmatched {unescaped} found"),
448        );
449    }
450
451    let mut token_stream = fmt_like.initial;
452
453    let positional_provided = fmt_like.exprs.len();
454    let mut exprs = fmt_like.exprs.into_iter();
455
456    let comma = TokenTree::Punct(Punct::new(',', Spacing::Alone));
457
458    for arg in args {
459        token_stream = match arg {
460            Arg::Str(string, range) => {
461                let mut str = Literal::string(&string);
462                str.set_span(str_span(range));
463
464                recurse_parser(
465                    &fmt_like.str_parser,
466                    token_stream
467                        .into_iter()
468                        .chain([comma.clone(), TokenTree::Literal(str)]),
469                )
470            }
471            Arg::Positional(p, range, modif) => {
472                if let Some(expr) = exprs.next() {
473                    recurse_parser(
474                        &fmt_like.arg_parsers[p].parser,
475                        token_stream
476                            .into_iter()
477                            .chain([comma.clone()])
478                            .chain(modif)
479                            .chain([comma.clone()])
480                            .chain(expr),
481                    )
482                } else {
483                    let npl = if positional_needed == 1 { "" } else { "s" };
484                    let pverb = if positional_provided == 1 {
485                        "is"
486                    } else {
487                        "are"
488                    };
489                    let ppl = if positional_provided == 1 { "" } else { "s" };
490
491                    return compile_err(
492                        str_span(range),
493                        format!(
494                            "{positional_needed} positional argument{npl} in format string, but there {pverb} {positional_provided} argument{ppl}"
495                        ),
496                    );
497                }
498            }
499            Arg::Inlined(p, idents, modif) => recurse_parser(
500                &fmt_like.arg_parsers[p].parser,
501                token_stream
502                    .into_iter()
503                    .chain([comma.clone()])
504                    .chain(modif)
505                    .chain([comma.clone()])
506                    .chain(idents),
507            ),
508        }
509    }
510
511    // There should be no positional arguments left.
512    if let Some(expr) = exprs.next() {
513        return compile_err(
514            expr.into_iter().next().unwrap().span(),
515            "argument never used",
516        );
517    }
518
519    token_stream
520}
521
522struct ArgParser {
523    delims: [char; 2],
524    delim_span: Span,
525    parser: Ident,
526    inline_only: bool,
527}
528
529struct FormatLike {
530    str_parser: Ident,
531    arg_parsers: Vec<ArgParser>,
532    initial: TokenStream,
533    str: StringLit<String>,
534    str_span: Span,
535    exprs: Vec<TokenStream>,
536}
537
538impl FormatLike {
539    fn parse(mut stream: impl Iterator<Item = TokenTree>) -> Result<Self, TokenStream> {
540        use TokenTree as TT;
541
542        let str_parser = get_ident(stream.next())?;
543
544        consume_comma(stream.next())?;
545
546        let arg_parsers = {
547            let group = match stream.next() {
548                Some(TT::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
549                Some(other) => return Err(compile_err(other.span(), "expected a list")),
550                _ => return Err(compile_err(Span::mixed_site(), "expected a list")),
551            };
552
553            let mut arg_parsers = Vec::new();
554
555            let mut stream = group.stream().into_iter();
556
557            loop {
558                static INVALID_ERR: &str = "expected one of '{', '(', '[', or '<'";
559
560                let group = match stream.next() {
561                    Some(TT::Group(group)) if group.delimiter() == Delimiter::Parenthesis => group,
562                    None => break,
563                    Some(other) => return Err(compile_err(other.span(), "expected a tuple")),
564                };
565
566                let mut substream = group.stream().into_iter();
567
568                let (delims, delim_span) = match substream.next() {
569                    Some(TT::Literal(literal)) => match literal.to_string().as_str() {
570                        "'{'" => (['{', '}'], literal.span()),
571                        "'('" => (['(', ')'], literal.span()),
572                        "'['" => (['[', ']'], literal.span()),
573                        "'<'" => (['<', '>'], literal.span()),
574                        _ => return Err(compile_err(literal.span(), INVALID_ERR)),
575                    },
576                    Some(other) => return Err(compile_err(other.span(), INVALID_ERR)),
577                    _ => return Err(compile_err(Span::mixed_site(), INVALID_ERR)),
578                };
579
580                consume_comma(substream.next())?;
581
582                let parser = get_ident(substream.next())?;
583
584                consume_comma(substream.next())?;
585
586                let inline_only = match substream.next() {
587                    Some(TT::Ident(ident)) => match ident.to_string().as_str() {
588                        "true" => true,
589                        "false" => false,
590                        _ => {
591                            return Err(compile_err(
592                                ident.span(),
593                                format!("expected a bool, got {ident:?}"),
594                            ));
595                        }
596                    },
597                    Some(other) => {
598                        return Err(compile_err(
599                            other.span(),
600                            format!("expected a bool, got {other}"),
601                        ));
602                    }
603                    _ => return Err(compile_err(Span::mixed_site(), "expected a bool")),
604                };
605
606                arg_parsers.push(ArgParser { delims, delim_span, parser, inline_only });
607
608                _ = consume_comma(stream.next());
609            }
610
611            arg_parsers
612        };
613
614        if let Some((lhs, rhs)) = arg_parsers.iter().enumerate().find_map(|(i, lhs)| {
615            arg_parsers.iter().enumerate().find_map(|(j, rhs)| {
616                (i != j)
617                    .then(|| (rhs.delims == lhs.delims).then_some((lhs, rhs)))
618                    .flatten()
619            })
620        }) {
621            return Err(TokenStream::from_iter([
622                compile_err(lhs.delim_span, "this delimiter"),
623                compile_err(rhs.delim_span, "is the same as this"),
624            ]));
625        }
626
627        consume_comma(stream.next())?;
628
629        let initial = {
630            let mut initial = Vec::new();
631
632            for token in stream.by_ref() {
633                if let TokenTree::Punct(punct) = &token
634                    && punct.as_char() == ','
635                {
636                    break;
637                }
638
639                initial.push(token);
640            }
641
642            TokenStream::from_iter(initial)
643        };
644
645        let (str, str_span) = match stream.next() {
646            Some(TokenTree::Literal(literal)) => {
647                match litrs::StringLit::parse(literal.to_string()) {
648                    Ok(str) => (str, literal.span()),
649                    Err(_) => return Err(compile_err(literal.span(), "expected a string literal")),
650                }
651            }
652            Some(other) => return Err(compile_err(other.span(), "expected a string literal")),
653            None => return Err(compile_err(Span::mixed_site(), "expected a string literal")),
654        };
655
656        let exprs = match stream.next() {
657            Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => {
658                let mut exprs = Vec::new();
659
660                let mut tokens = Vec::new();
661
662                for token in stream {
663                    if let TokenTree::Punct(punct) = &token
664                        && punct.as_char() == ','
665                    {
666                        if !tokens.is_empty() {
667                            exprs.push(TokenStream::from_iter(tokens.drain(..)));
668                        }
669                    } else {
670                        tokens.push(token);
671                    }
672                }
673
674                if !tokens.is_empty() {
675                    exprs.push(TokenStream::from_iter(tokens));
676                }
677
678                exprs
679            }
680            Some(other) => return Err(compile_err(other.span(), "expected a comma")),
681            None => Vec::new(),
682        };
683
684        Ok(Self {
685            str_parser,
686            arg_parsers,
687            initial,
688            str,
689            str_span,
690            exprs,
691        })
692    }
693}
694
695enum Arg {
696    Str(String, Range<usize>),
697    Positional(usize, Range<usize>, TokenStream),
698    Inlined(usize, TokenStream, TokenStream),
699}
700
701fn recurse_parser(parser: &Ident, stream: impl Iterator<Item = TokenTree>) -> TokenStream {
702    let start = parser.span().start();
703
704    TokenStream::from_iter([
705        TokenTree::Ident(parser.clone()),
706        TokenTree::Punct({
707            let mut punct = Punct::new('!', Spacing::Alone);
708            punct.set_span(start);
709            punct
710        }),
711        TokenTree::Group(Group::new(Delimiter::Parenthesis, stream.collect())),
712    ])
713}
714
715fn consume_comma(value: Option<TokenTree>) -> Result<(), TokenStream> {
716    match value {
717        Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => Ok(()),
718        Some(other) => Err(compile_err(other.span(), "Expected a comma")),
719        _ => Err(compile_err(Span::mixed_site(), "Expected a comma")),
720    }
721}
722
723fn get_ident(value: Option<TokenTree>) -> Result<Ident, TokenStream> {
724    match value {
725        Some(TokenTree::Ident(ident)) => Ok(ident),
726        Some(other) => Err(compile_err(other.span(), "Expected an identifier")),
727        _ => Err(compile_err(Span::mixed_site(), "Expected an identifier")),
728    }
729}
730
731fn extend_str_arg(args: &mut Vec<Arg>, char: char, i: usize) {
732    if let Some(Arg::Str(string, range)) = args.last_mut() {
733        string.push(char);
734        range.end = i + 1;
735    } else {
736        args.push(Arg::Str(String::from(char), i..i + 1))
737    }
738}
739
740fn compile_err(span: Span, msg: impl std::fmt::Display) -> TokenStream {
741    let (start, end) = (span.start(), span.end());
742
743    TokenStream::from_iter([
744        TokenTree::Punct({
745            let mut punct = Punct::new(':', Spacing::Joint);
746            punct.set_span(start);
747            punct
748        }),
749        TokenTree::Punct({
750            let mut punct = Punct::new(':', Spacing::Alone);
751            punct.set_span(start);
752            punct
753        }),
754        TokenTree::Ident(Ident::new("core", start)),
755        TokenTree::Punct({
756            let mut punct = Punct::new(':', Spacing::Joint);
757            punct.set_span(start);
758            punct
759        }),
760        TokenTree::Punct({
761            let mut punct = Punct::new(':', Spacing::Alone);
762            punct.set_span(start);
763            punct
764        }),
765        TokenTree::Ident({
766            let mut ident = Ident::new("compile_error", start);
767            ident.set_span(start);
768            ident
769        }),
770        TokenTree::Punct({
771            let mut punct = Punct::new('!', Spacing::Alone);
772            punct.set_span(start);
773            punct
774        }),
775        TokenTree::Group({
776            let mut group = Group::new(Delimiter::Brace, {
777                TokenStream::from_iter([TokenTree::Literal({
778                    let mut string = Literal::string(&msg.to_string());
779                    string.set_span(end);
780                    string
781                })])
782            });
783            group.set_span(end);
784            group
785        }),
786    ])
787}
788
789type CurrentArg = (
790    usize,
791    usize,
792    Vec<(Range<usize>, bool)>,
793    Option<Range<usize>>,
794);