Skip to main content

granit_parser/
input.rs

1//! Utilities to create a source of input to the parser.
2//!
3//! [`Input`] must be implemented for the parser to fetch input. Make sure your needs aren't
4//! covered by the [`BufferedInput`].
5
6use alloc::string::String;
7
8pub(crate) mod buffered;
9pub(crate) mod str;
10
11#[allow(clippy::module_name_repetitions)]
12pub use buffered::BufferedInput;
13
14/// A trait for inputs that can provide borrowed slices with a specific lifetime.
15///
16/// This trait enables zero-copy (`Cow::Borrowed`) token values for inputs that keep a stable
17/// backing string. The key difference from [`Input::slice_bytes`] is that this method returns
18/// a slice with the input's original lifetime `'a`, not tied to `&self`.
19///
20/// For inputs that support zero-copy (like [`str::StrInput`]), this returns `Some(&'a str)`.
21/// For streaming inputs that don't have stable backing storage, this returns `None`.
22pub trait BorrowedInput<'a>: Input {
23    /// Return a borrowed slice of the underlying source between two byte offsets.
24    ///
25    /// Unlike [`Input::slice_bytes`], this returns a slice with the input's lifetime `'a`,
26    /// allowing the slice to outlive the borrow of `&self`.
27    ///
28    /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
29    /// half-open: `[start, end)`.
30    ///
31    /// Returns `None` if the input does not support zero-copy slicing.
32    #[must_use]
33    fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'a str>;
34}
35
36pub use crate::char_traits::{
37    is_alpha, is_blank, is_blank_or_breakz, is_break, is_breakz, is_digit, is_flow, is_z,
38};
39
40/// Interface for a source of characters.
41///
42/// Hiding the input's implementation behind this trait allows input-specific optimizations, such
43/// as using `str` methods instead of manually transferring one `char` at a time to a buffer.
44/// Implementations with stable backing storage can also return borrowed `&str` slices and avoid
45/// allocating token values.
46pub trait Input {
47    /// A hint to the input source that we will need to read `count` characters.
48    ///
49    /// If the input is exhausted, `\0` can be used to pad the last characters and later returned.
50    /// The characters must not be consumed, but may be placed in an internal buffer.
51    ///
52    /// This method may be a no-op if buffering yields no performance improvement.
53    ///
54    /// Implementers of [`Input`] must _not_ expose a lookahead window larger than
55    /// [`Input::bufmaxlen`]. They may retain a larger window requested by an earlier call; callers
56    /// should use [`Input::buflen`] to observe the currently available window.
57    fn lookahead(&mut self, count: usize);
58
59    /// Return the number of characters in the active lookahead window.
60    ///
61    /// This is the number of characters that the input promises can be read through [`peek`] and
62    /// [`peek_nth`] after prior [`lookahead`] calls. It is not necessarily the number of source
63    /// characters remaining: inputs may keep the window available after consuming characters and
64    /// may pad positions past EOF with `\0`.
65    ///
66    /// [`lookahead`]: Input::lookahead
67    /// [`peek`]: Input::peek
68    /// [`peek_nth`]: Input::peek_nth
69    #[must_use]
70    fn buflen(&self) -> usize;
71
72    /// Return the maximum number of characters this input can buffer for lookahead.
73    #[must_use]
74    fn bufmaxlen(&self) -> usize;
75
76    /// Return whether the active lookahead window is empty.
77    ///
78    /// This is equivalent to `self.buflen() == 0`. It does not mean the underlying source is
79    /// exhausted: after a previous [`lookahead`] call, an input may keep a non-empty lookahead
80    /// window available even after all source characters have been consumed, with positions past
81    /// EOF observed as `\0`.
82    ///
83    /// [`lookahead`]: Input::lookahead
84    #[inline]
85    #[must_use]
86    fn buf_is_empty(&self) -> bool {
87        self.buflen() == 0
88    }
89
90    /// Read the next character from the logical input stream and return it directly.
91    ///
92    /// If an implementation has already fetched characters for lookahead, this consumes the
93    /// buffered stream front before reading farther from the underlying source.
94    #[must_use]
95    fn raw_read_ch(&mut self) -> char;
96
97    /// Read a non-breakz character from the input stream and return it directly.
98    ///
99    /// If an implementation has already fetched characters for lookahead, this consumes from the
100    /// buffered stream front before reading farther from the underlying source.
101    ///
102    /// If the next character is a breakz, it is either not consumed or placed into the buffer (if
103    /// any).
104    #[must_use]
105    fn raw_read_non_breakz_ch(&mut self) -> Option<char>;
106
107    /// Consume the next character.
108    fn skip(&mut self);
109
110    /// Consume the next `count` characters.
111    fn skip_n(&mut self, count: usize);
112
113    /// Return the next character, without consuming it.
114    ///
115    /// Users of the [`Input`] must make sure that the character has been loaded through a prior
116    /// call to [`Input::lookahead`]. Implementors of [`Input`] may assume that a valid call to
117    /// [`Input::lookahead`] has been made beforehand.
118    ///
119    /// # Return
120    /// If the input source is not exhausted, returns the next character to be fed into the
121    /// scanner. Otherwise, returns `\0`.
122    #[must_use]
123    fn peek(&self) -> char;
124
125    /// Return the `n`-th character in the buffer, without consuming it.
126    ///
127    /// This function assumes that the `n`-th character in the input has already been fetched through
128    /// [`Input::lookahead`].
129    #[must_use]
130    fn peek_nth(&self, n: usize) -> char;
131
132    /// Return the current byte offset in the underlying source, if available.
133    ///
134    /// This is an *optional* capability that enables zero-copy (`Cow::Borrowed`) token values
135    /// for inputs that keep a stable backing string (notably [`str::StrInput`]).
136    ///
137    /// The returned value (when `Some`) is the number of bytes that have been consumed so far,
138    /// i.e. an offset into the original source string.
139    ///
140    /// # Correctness contract
141    /// Implementations returning `Some(_)` must satisfy all of the following:
142    ///
143    /// - The offset is a valid UTF-8 boundary in the underlying source.
144    /// - The offset is monotonically non-decreasing as characters are consumed.
145    /// - The underlying source is stable for the duration of parsing (no reallocation/mutation)
146    ///   so that slices returned by [`Input::slice_bytes`] remain valid.
147    ///
148    /// Inputs that cannot provide stable slicing (e.g. stream/iterator inputs) must return
149    /// `None`.
150    #[inline]
151    #[must_use]
152    fn byte_offset(&self) -> Option<usize> {
153        None
154    }
155
156    /// Return a borrowed slice of the underlying source between two byte offsets.
157    ///
158    /// This is an *optional* capability used to produce `Cow::Borrowed` values without
159    /// allocating.
160    ///
161    /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
162    /// half-open: `[start, end)`.
163    ///
164    /// # Correctness contract
165    /// Implementations returning `Some(&str)` must ensure:
166    ///
167    /// - `start <= end`.
168    /// - Both offsets are valid UTF-8 boundaries.
169    /// - The returned `&str` is a view into the stable underlying source associated with this
170    ///   input.
171    ///
172    /// Implementations that return `None` from [`Input::byte_offset`] must also return `None`
173    /// here.
174    #[inline]
175    #[must_use]
176    fn slice_bytes(&self, _start: usize, _end: usize) -> Option<&str> {
177        None
178    }
179
180    /// Return whether this input may contain a `#` character.
181    ///
182    /// This is a conservative performance hint. Inputs that cannot answer cheaply should return
183    /// `true`, which keeps full comment handling enabled.
184    #[inline]
185    #[must_use]
186    fn may_contain_comments(&self) -> bool {
187        true
188    }
189
190    /// Look for the next character and return it.
191    ///
192    /// The character is not consumed.
193    /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
194    #[inline]
195    #[must_use]
196    fn look_ch(&mut self) -> char {
197        self.lookahead(1);
198        self.peek()
199    }
200
201    /// Return whether the next character in the input source is equal to `c`.
202    ///
203    /// This function assumes that the next character in the input has already been fetched through
204    /// [`Input::lookahead`].
205    #[inline]
206    #[must_use]
207    fn next_char_is(&self, c: char) -> bool {
208        self.peek() == c
209    }
210
211    /// Return whether the `n`-th character in the input source is equal to `c`.
212    ///
213    /// This function assumes that the `n`-th character in the input has already been fetched through
214    /// [`Input::lookahead`].
215    #[inline]
216    #[must_use]
217    fn nth_char_is(&self, n: usize, c: char) -> bool {
218        self.peek_nth(n) == c
219    }
220
221    /// Return whether the next 2 characters in the input source match the given characters.
222    ///
223    /// This function assumes that the next 2 characters in the input have already been fetched
224    /// through [`Input::lookahead`].
225    #[inline]
226    #[must_use]
227    fn next_2_are(&self, c1: char, c2: char) -> bool {
228        assert!(self.buflen() >= 2);
229        self.peek() == c1 && self.peek_nth(1) == c2
230    }
231
232    /// Return whether the next 3 characters in the input source match the given characters.
233    ///
234    /// This function assumes that the next 3 characters in the input have already been fetched
235    /// through [`Input::lookahead`].
236    #[inline]
237    #[must_use]
238    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
239        assert!(self.buflen() >= 3);
240        self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
241    }
242
243    /// Check whether the next characters correspond to a document indicator.
244    ///
245    /// This function assumes that the next 4 characters in the input have already been fetched
246    /// through [`Input::lookahead`].
247    #[inline]
248    #[must_use]
249    fn next_is_document_indicator(&self) -> bool {
250        assert!(self.buflen() >= 4);
251        is_blank_or_breakz(self.peek_nth(3))
252            && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
253    }
254
255    /// Check whether the next characters correspond to a start of document.
256    ///
257    /// This function assumes that the next 4 characters in the input have already been fetched
258    /// through [`Input::lookahead`].
259    #[inline]
260    #[must_use]
261    fn next_is_document_start(&self) -> bool {
262        assert!(self.buflen() >= 4);
263        self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
264    }
265
266    /// Check whether the next characters correspond to an end of document.
267    ///
268    /// This function assumes that the next 4 characters in the input have already been fetched
269    /// through [`Input::lookahead`].
270    #[inline]
271    #[must_use]
272    fn next_is_document_end(&self) -> bool {
273        assert!(self.buflen() >= 4);
274        self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
275    }
276
277    /// Skip YAML whitespace up to the end of the current line.
278    ///
279    /// Inline comments are consumed only after at least one preceding YAML whitespace character.
280    ///
281    /// # Return
282    /// Return a tuple with the number of characters that were consumed and the result of skipping
283    /// whitespace. The number of characters returned can be used to advance the index and column,
284    /// since no end-of-line character will be consumed.
285    /// See [`SkipTabs`] for more details on the success variant.
286    ///
287    /// # Errors
288    /// Errors if a comment is encountered but it was not preceded by a whitespace. In that event,
289    /// the first tuple element will contain the number of characters consumed prior to reaching
290    /// the `#`.
291    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
292        let mut encountered_tab = false;
293        let mut has_yaml_ws = false;
294        let mut chars_consumed = 0;
295        loop {
296            match self.look_ch() {
297                ' ' => {
298                    has_yaml_ws = true;
299                    self.skip();
300                }
301                '\t' if skip_tabs != SkipTabs::No => {
302                    encountered_tab = true;
303                    self.skip();
304                }
305                // YAML comments must be preceded by whitespace.
306                '#' if !encountered_tab && !has_yaml_ws => {
307                    return (
308                        chars_consumed,
309                        Err("comments must be separated from other tokens by whitespace"),
310                    );
311                }
312                '#' => {
313                    self.skip(); // Skip over '#'
314                    while !is_breakz(self.look_ch()) {
315                        self.skip();
316                        chars_consumed += 1;
317                    }
318                }
319                _ => break,
320            }
321            chars_consumed += 1;
322        }
323
324        (
325            chars_consumed,
326            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
327        )
328    }
329
330    /// Skip YAML blank characters, stopping before comments, line breaks, or other content.
331    ///
332    /// This is the comment-aware counterpart to [`Input::skip_ws_to_eol`]: it preserves a
333    /// following `#` for the scanner to tokenize while still letting input implementations batch
334    /// the common run of spaces and tabs.
335    ///
336    /// # Return
337    /// Returns the number of consumed characters and a [`SkipTabs::Result`] describing whether
338    /// tabs and valid YAML whitespace (` `) were encountered.
339    fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, SkipTabs) {
340        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
341
342        let mut encountered_tab = false;
343        let mut has_yaml_ws = false;
344        let mut chars_consumed = 0;
345
346        loop {
347            match self.look_ch() {
348                ' ' => {
349                    has_yaml_ws = true;
350                    chars_consumed += 1;
351                    self.skip();
352                }
353                '\t' if skip_tabs != SkipTabs::No => {
354                    encountered_tab = true;
355                    chars_consumed += 1;
356                    self.skip();
357                }
358                _ => break,
359            }
360        }
361
362        (
363            chars_consumed,
364            SkipTabs::Result(encountered_tab, has_yaml_ws),
365        )
366    }
367
368    /// Check whether the next characters may be part of a plain scalar.
369    ///
370    /// This function assumes we are not given a blankz character.
371    #[allow(clippy::inline_always)]
372    #[inline(always)]
373    fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
374        let nc = self.peek_nth(1);
375        match self.peek() {
376            // indicators can end a plain scalar, see 7.3.3. Plain Style
377            ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
378            c if in_flow && is_flow(c) => false,
379            _ => true,
380        }
381    }
382
383    /// Check whether the next character is [a blank] or [a break].
384    ///
385    /// The character must have previously been fetched through [`lookahead`]
386    ///
387    /// # Return
388    /// Returns true if the character is [a blank] or [a break], false otherwise.
389    ///
390    /// [`lookahead`]: Input::lookahead
391    /// [a blank]: is_blank
392    /// [a break]: is_break
393    #[inline]
394    fn next_is_blank_or_break(&self) -> bool {
395        is_blank(self.peek()) || is_break(self.peek())
396    }
397
398    /// Check whether the next character is [a blank] or [a breakz].
399    ///
400    /// The character must have previously been fetched through [`lookahead`]
401    ///
402    /// # Return
403    /// Returns true if the character is [a blank] or [a break], false otherwise.
404    ///
405    /// [`lookahead`]: Input::lookahead
406    /// [a blank]: is_blank
407    /// [a breakz]: is_breakz
408    #[inline]
409    fn next_is_blank_or_breakz(&self) -> bool {
410        is_blank(self.peek()) || is_breakz(self.peek())
411    }
412
413    /// Check whether the next character is [a blank].
414    ///
415    /// The character must have previously been fetched through [`lookahead`]
416    ///
417    /// # Return
418    /// Returns true if the character is [a blank], false otherwise.
419    ///
420    /// [`lookahead`]: Input::lookahead
421    /// [a blank]: is_blank
422    #[inline]
423    fn next_is_blank(&self) -> bool {
424        is_blank(self.peek())
425    }
426
427    /// Check whether the next character is [a break].
428    ///
429    /// The character must have previously been fetched through [`lookahead`]
430    ///
431    /// # Return
432    /// Returns true if the character is [a break], false otherwise.
433    ///
434    /// [`lookahead`]: Input::lookahead
435    /// [a break]: is_break
436    #[inline]
437    fn next_is_break(&self) -> bool {
438        is_break(self.peek())
439    }
440
441    /// Check whether the next character is [a breakz].
442    ///
443    /// The character must have previously been fetched through [`lookahead`]
444    ///
445    /// # Return
446    /// Returns true if the character is [a breakz], false otherwise.
447    ///
448    /// [`lookahead`]: Input::lookahead
449    /// [a breakz]: is_breakz
450    #[inline]
451    fn next_is_breakz(&self) -> bool {
452        is_breakz(self.peek())
453    }
454
455    /// Check whether the next character is [a z].
456    ///
457    /// The character must have previously been fetched through [`lookahead`]
458    ///
459    /// # Return
460    /// Returns true if the character is [a z], false otherwise.
461    ///
462    /// [`lookahead`]: Input::lookahead
463    /// [a z]: is_z
464    #[inline]
465    fn next_is_z(&self) -> bool {
466        is_z(self.peek())
467    }
468
469    /// Check whether the next character is [a flow].
470    ///
471    /// The character must have previously been fetched through [`lookahead`]
472    ///
473    /// # Return
474    /// Returns true if the character is [a flow], false otherwise.
475    ///
476    /// [`lookahead`]: Input::lookahead
477    /// [a flow]: is_flow
478    #[inline]
479    fn next_is_flow(&self) -> bool {
480        is_flow(self.peek())
481    }
482
483    /// Check whether the next character is [a digit].
484    ///
485    /// The character must have previously been fetched through [`lookahead`]
486    ///
487    /// # Return
488    /// Returns true if the character is [a digit], false otherwise.
489    ///
490    /// [`lookahead`]: Input::lookahead
491    /// [a digit]: is_digit
492    #[inline]
493    fn next_is_digit(&self) -> bool {
494        is_digit(self.peek())
495    }
496
497    /// Check whether the next character is [a letter].
498    ///
499    /// The character must have previously been fetched through [`lookahead`]
500    ///
501    /// # Return
502    /// Returns true if the character is [a letter], false otherwise.
503    ///
504    /// [`lookahead`]: Input::lookahead
505    /// [a letter]: is_alpha
506    #[inline]
507    fn next_is_alpha(&self) -> bool {
508        is_alpha(self.peek())
509    }
510
511    /// Skip characters from the input until a [breakz] is found.
512    ///
513    /// The characters are consumed from the input.
514    ///
515    /// # Return
516    /// Return the number of characters that were consumed. The number of characters returned can
517    /// be used to advance the index and column, since no end-of-line character will be consumed.
518    ///
519    /// [breakz]: is_breakz
520    #[inline]
521    fn skip_while_non_breakz(&mut self) -> usize {
522        let mut count = 0;
523        while !is_breakz(self.look_ch()) {
524            count += 1;
525            self.skip();
526        }
527        count
528    }
529
530    /// Skip characters from the input while [blanks] are found.
531    ///
532    /// The characters are consumed from the input.
533    ///
534    /// # Return
535    /// Return the number of characters that were consumed. The number of characters returned can
536    /// be used to advance the index and column, since no end-of-line character will be consumed.
537    ///
538    /// [blanks]: is_blank
539    fn skip_while_blank(&mut self) -> usize {
540        let mut n_bytes = 0;
541        while is_blank(self.look_ch()) {
542            n_bytes += self.peek().len_utf8();
543            self.skip();
544        }
545        n_bytes
546    }
547
548    /// Fetch characters from the input while we encounter letters and store them in `out`.
549    ///
550    /// The characters are consumed from the input.
551    ///
552    /// # Return
553    /// Return the number of characters that were consumed. The number of characters returned can
554    /// be used to advance the index and column, since no end-of-line character will be consumed.
555    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
556        let mut n_bytes = 0;
557        while is_alpha(self.look_ch()) {
558            let c = self.peek();
559            n_bytes += c.len_utf8();
560            out.push(c);
561            self.skip();
562        }
563        n_bytes
564    }
565
566    /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
567    ///
568    /// The characters are consumed from the input.
569    ///
570    /// # Return
571    /// Return the number of characters that were consumed. The number of characters returned can
572    /// be used to advance the index and column, since no end-of-line character will be consumed.
573    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
574        let mut chars_consumed = 0;
575        loop {
576            let c = self.look_ch();
577            if !crate::char_traits::is_yaml_non_space(c) || is_z(c) {
578                break;
579            }
580            let c = self.peek();
581            out.push(c);
582            self.skip();
583            chars_consumed += 1;
584        }
585        chars_consumed
586    }
587
588    /// Fetch a chunk of plain scalar characters.
589    ///
590    /// This optimization method allows the input to batch process characters.
591    /// Returns (stopped, `chars_consumed`).
592    /// stopped is true if the chunk ended because of a non-plain-scalar character.
593    fn fetch_plain_scalar_chunk(
594        &mut self,
595        out: &mut String,
596        count: usize,
597        flow_level_gt_0: bool,
598    ) -> (bool, usize) {
599        let mut chars_consumed = 0;
600        for _ in 0..count {
601            self.lookahead(1);
602            if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) {
603                return (true, chars_consumed);
604            }
605            out.push(self.peek());
606            self.skip();
607            chars_consumed += 1;
608        }
609        (false, chars_consumed)
610    }
611}
612
613/// Behavior to adopt regarding treating tabs as whitespace.
614///
615/// Although tab is valid YAML whitespace, it does not always behave the same as a space.
616#[derive(Copy, Clone, Eq, PartialEq)]
617pub enum SkipTabs {
618    /// Skip all tabs as whitespace.
619    Yes,
620    /// Don't skip any tab. Return from the function when encountering one.
621    No,
622    /// Return value from the function.
623    Result(
624        /// Whether tabs were encountered.
625        bool,
626        /// Whether at least one valid YAML whitespace character has been encountered.
627        bool,
628    ),
629}
630
631impl SkipTabs {
632    /// Whether tabs were found while skipping whitespace.
633    ///
634    /// This function must be called after a call to `skip_ws_to_eol`.
635    #[must_use]
636    pub fn found_tabs(self) -> bool {
637        matches!(self, SkipTabs::Result(true, _))
638    }
639
640    /// Whether a valid YAML whitespace has been found in skipped-over content.
641    ///
642    /// This function must be called after a call to `skip_ws_to_eol`.
643    #[must_use]
644    pub fn has_valid_yaml_ws(self) -> bool {
645        matches!(self, SkipTabs::Result(_, true))
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use super::{Input, SkipTabs};
652
653    struct MinimalInput;
654
655    impl Input for MinimalInput {
656        fn lookahead(&mut self, _count: usize) {}
657
658        fn buflen(&self) -> usize {
659            0
660        }
661
662        fn bufmaxlen(&self) -> usize {
663            0
664        }
665
666        fn raw_read_ch(&mut self) -> char {
667            '\0'
668        }
669
670        fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
671            None
672        }
673
674        fn skip(&mut self) {}
675
676        fn skip_n(&mut self, _count: usize) {}
677
678        fn peek(&self) -> char {
679            '\0'
680        }
681
682        fn peek_nth(&self, _n: usize) -> char {
683            '\0'
684        }
685    }
686
687    #[test]
688    fn default_slice_bytes_returns_none() {
689        let mut input = MinimalInput;
690
691        input.lookahead(4);
692        assert_eq!(input.buflen(), 0);
693        assert_eq!(input.bufmaxlen(), 0);
694        assert_eq!(input.raw_read_ch(), '\0');
695        assert_eq!(input.raw_read_non_breakz_ch(), None);
696        input.skip();
697        input.skip_n(2);
698        assert_eq!(input.peek(), '\0');
699        assert_eq!(input.peek_nth(1), '\0');
700        assert_eq!(input.byte_offset(), None);
701        assert_eq!(input.slice_bytes(0, 0), None);
702    }
703
704    #[test]
705    fn default_skip_ws_to_eol_rejects_unseparated_comment() {
706        let mut input = super::buffered::BufferedInput::new("#comment\n".chars());
707
708        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
709
710        assert_eq!(consumed, 0);
711        assert_eq!(
712            result.err(),
713            Some("comments must be separated from other tokens by whitespace")
714        );
715        assert_eq!(input.peek(), '#');
716    }
717}