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}