Skip to main content

rucc_parse/
cursor.rs

1//! The token buffer the parser reads.
2//!
3//! Design: `spec/06-lexer-and-parser.md` section 6.3.
4//!
5//! The input is the slice of tokens phase 7 produced, which always ends in one
6//! [`TokenKind::Eof`]. A cursor is a position in that slice and nothing more: it holds no
7//! parser state, it builds nothing and it reports nothing. That is what makes saving one and
8//! restoring it safe, and it is why the recovery skips in [`crate::recover`] take a cursor
9//! rather than the whole parser.
10//!
11//! # Why the lookahead is bounded
12//!
13//! Unbounded backtracking is how a C parser becomes quadratic on the input a fuzzer eventually
14//! finds, so [`Cursor::peek`] refuses to look further than [`MAX_LOOKAHEAD`] tokens ahead and
15//! panics rather than quietly widening the window. The bound is a budget rather than a fact
16//! about the grammar: a decision that cannot be made inside it is either a save and a restore,
17//! which is deliberate and rare, or a sign that the decision is being made in the wrong place,
18//! and the panic is how that conversation starts.
19
20use rucc_diag::Span;
21use rucc_lex::{Keyword, Punct, Token, TokenKind};
22
23/// How far ahead [`Cursor::peek`] will look.
24pub const MAX_LOOKAHEAD: usize = 4;
25
26/// A saved position, taken by [`Cursor::save`] and given back to [`Cursor::restore`].
27///
28/// Opaque on purpose. A position is only meaningful for the cursor that produced it, and an
29/// arbitrary index into the token stream is not something the parser should be able to invent.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Mark(usize);
32
33/// A position in the token stream.
34#[derive(Debug, Clone)]
35pub struct Cursor<'a> {
36    tokens: &'a [Token],
37    at: usize,
38}
39
40impl<'a> Cursor<'a> {
41    /// A cursor on the first token of `tokens`.
42    ///
43    /// # Panics
44    ///
45    /// Panics if `tokens` does not end in [`TokenKind::Eof`]. Every method here relies on that
46    /// token being there: it is what a peek past the end returns and it is what stops every
47    /// recovery skip, so a stream without one would turn a malformed file into a hang.
48    #[must_use]
49    pub fn new(tokens: &'a [Token]) -> Self {
50        assert!(
51            tokens.last().is_some_and(|token| token.is_eof()),
52            "the token stream must end in `Eof`"
53        );
54        Cursor { tokens, at: 0 }
55    }
56
57    /// The token the parser is looking at.
58    #[inline]
59    #[must_use]
60    pub fn current(&self) -> Token {
61        self.tokens[self.at]
62    }
63
64    /// The token `n` places ahead, which is the final [`TokenKind::Eof`] once the end is
65    /// reached rather than an out of range access.
66    ///
67    /// # Panics
68    ///
69    /// Panics if `n` is greater than [`MAX_LOOKAHEAD`].
70    #[inline]
71    #[must_use]
72    pub fn peek(&self, n: usize) -> Token {
73        assert!(n <= MAX_LOOKAHEAD, "lookahead of {n} tokens, past the bound of {MAX_LOOKAHEAD}");
74        self.tokens[(self.at + n).min(self.tokens.len() - 1)]
75    }
76
77    /// Where the current token is, which is where a diagnostic about it points.
78    #[inline]
79    #[must_use]
80    pub fn span(&self) -> Span {
81        self.current().span
82    }
83
84    /// The empty span just after the previous token, which is where something that should have
85    /// been written and was not belongs.
86    ///
87    /// Pointing a missing semicolon at the token that follows it is a small thing that reads
88    /// badly, because the token that follows is usually on the next line and is not the
89    /// problem. Before the first token there is no previous one, so this is the start of the
90    /// current token instead.
91    #[must_use]
92    pub fn prev_end(&self) -> Span {
93        match self.at.checked_sub(1) {
94            Some(prev) => Span::empty_at(self.tokens[prev].span.hi),
95            None => Span::empty_at(self.current().span.lo),
96        }
97    }
98
99    /// Whether the parser has reached the end of the translation unit.
100    #[inline]
101    #[must_use]
102    pub fn is_eof(&self) -> bool {
103        self.current().is_eof()
104    }
105
106    /// Whether the current token is exactly `kind`.
107    #[inline]
108    #[must_use]
109    pub fn at(&self, kind: TokenKind) -> bool {
110        self.current().kind == kind
111    }
112
113    /// Whether the current token is the punctuator `punct`.
114    #[inline]
115    #[must_use]
116    pub fn at_punct(&self, punct: Punct) -> bool {
117        self.current().punct() == Some(punct)
118    }
119
120    /// Whether the current token is the keyword `keyword`.
121    #[inline]
122    #[must_use]
123    pub fn at_keyword(&self, keyword: Keyword) -> bool {
124        self.current().keyword() == Some(keyword)
125    }
126
127    /// Steps over the current token and gives it back.
128    ///
129    /// Stepping over the end is not an error and does not move: the cursor stays on the final
130    /// [`TokenKind::Eof`], so a loop that forgets to check for the end runs out of tokens
131    /// instead of reading off the end of the slice. It will still spin, which is what
132    /// [`Cursor::index`] is for.
133    #[inline]
134    pub fn bump(&mut self) -> Token {
135        let token = self.current();
136        if !token.is_eof() {
137            self.at += 1;
138        }
139        token
140    }
141
142    /// Steps over the current token if it is `kind`, and reports whether it did.
143    #[inline]
144    pub fn eat(&mut self, kind: TokenKind) -> bool {
145        let matched = self.at(kind);
146        if matched {
147            self.bump();
148        }
149        matched
150    }
151
152    /// Steps over the current token if it is the punctuator `punct`.
153    #[inline]
154    pub fn eat_punct(&mut self, punct: Punct) -> bool {
155        self.eat(TokenKind::Punct(punct))
156    }
157
158    /// Steps over the current token if it is the keyword `keyword`.
159    #[inline]
160    pub fn eat_keyword(&mut self, keyword: Keyword) -> bool {
161        self.eat(TokenKind::Keyword(keyword))
162    }
163
164    /// How many tokens the cursor has stepped over.
165    ///
166    /// The parser's loops compare this across an iteration to check that they made progress. A
167    /// production that returns without consuming anything is the classic way a recursive
168    /// descent parser hangs on malformed input, and it is a bug in the parser rather than
169    /// something to recover from, so the check belongs in an assertion and not in a `if`.
170    #[inline]
171    #[must_use]
172    pub fn index(&self) -> usize {
173        self.at
174    }
175
176    /// The current position, to be given back to [`Cursor::restore`].
177    #[inline]
178    #[must_use]
179    pub fn save(&self) -> Mark {
180        Mark(self.at)
181    }
182
183    /// Goes back to a saved position.
184    ///
185    /// This is not a general backtrack and it does not undo anything but the position. Between
186    /// a save and a restore the parser must not report a diagnostic and must not put a node in
187    /// the tree, because neither is taken back, and a speculative parse that leaves either
188    /// behind produces an error about a reading of the source that was abandoned. The two
189    /// constructs that need this are in `spec/06-lexer-and-parser.md` section 6.4.
190    ///
191    /// # Panics
192    ///
193    /// Panics if `mark` came from a cursor on a different token stream.
194    #[inline]
195    pub fn restore(&mut self, mark: Mark) {
196        assert!(mark.0 < self.tokens.len(), "restoring a mark from another token stream");
197        self.at = mark.0;
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use rucc_lex::TokenFlags;
204
205    use super::*;
206
207    /// A stream of punctuators, one byte each, ending in `Eof`.
208    fn stream(puncts: &[Punct]) -> Vec<Token> {
209        let mut tokens: Vec<Token> = puncts
210            .iter()
211            .enumerate()
212            .map(|(i, &punct)| Token {
213                kind: TokenKind::Punct(punct),
214                flags: TokenFlags::EMPTY,
215                value: 0,
216                span: Span::new(i as u32, i as u32 + 1),
217            })
218            .collect();
219        let end = puncts.len() as u32;
220        tokens.push(Token {
221            kind: TokenKind::Eof,
222            flags: TokenFlags::EMPTY,
223            value: 0,
224            span: Span::empty_at(end),
225        });
226        tokens
227    }
228
229    #[test]
230    fn peeking_past_the_end_gives_eof() {
231        let tokens = stream(&[Punct::Semi]);
232        let cursor = Cursor::new(&tokens);
233        assert!(cursor.peek(0).punct() == Some(Punct::Semi));
234        assert!(cursor.peek(1).is_eof());
235        assert!(cursor.peek(MAX_LOOKAHEAD).is_eof());
236    }
237
238    #[test]
239    fn bumping_stops_on_the_end() {
240        let tokens = stream(&[Punct::Semi]);
241        let mut cursor = Cursor::new(&tokens);
242        assert!(cursor.bump().punct() == Some(Punct::Semi));
243        for _ in 0..3 {
244            assert!(cursor.bump().is_eof());
245        }
246        assert_eq!(cursor.index(), 1);
247    }
248
249    #[test]
250    fn eating_only_moves_when_it_matches() {
251        let tokens = stream(&[Punct::Semi, Punct::Comma]);
252        let mut cursor = Cursor::new(&tokens);
253        assert!(!cursor.eat_punct(Punct::Comma));
254        assert_eq!(cursor.index(), 0);
255        assert!(cursor.eat_punct(Punct::Semi));
256        assert!(cursor.at_punct(Punct::Comma));
257        assert!(!cursor.eat_keyword(Keyword::Int));
258    }
259
260    #[test]
261    fn restoring_puts_the_cursor_back() {
262        let tokens = stream(&[Punct::LParen, Punct::Star, Punct::RParen]);
263        let mut cursor = Cursor::new(&tokens);
264        let mark = cursor.save();
265        cursor.bump();
266        cursor.bump();
267        assert!(cursor.at_punct(Punct::RParen));
268        cursor.restore(mark);
269        assert!(cursor.at_punct(Punct::LParen));
270    }
271
272    #[test]
273    fn a_missing_token_belongs_after_the_one_before_it() {
274        let tokens = stream(&[Punct::LParen, Punct::RParen]);
275        let mut cursor = Cursor::new(&tokens);
276        assert_eq!(cursor.prev_end(), Span::empty_at(0));
277        cursor.bump();
278        assert_eq!(cursor.prev_end(), Span::empty_at(1));
279    }
280
281    #[test]
282    #[should_panic(expected = "past the bound")]
283    fn looking_too_far_ahead_is_a_bug() {
284        let tokens = stream(&[Punct::Semi]);
285        let _ = Cursor::new(&tokens).peek(MAX_LOOKAHEAD + 1);
286    }
287
288    #[test]
289    #[should_panic(expected = "must end in `Eof`")]
290    fn a_stream_without_an_end_is_rejected() {
291        let _ = Cursor::new(&[]);
292    }
293}