use rucc_diag::Span;
use rucc_lex::{Keyword, Punct, Token, TokenKind};
pub const MAX_LOOKAHEAD: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Mark(usize);
#[derive(Debug, Clone)]
pub struct Cursor<'a> {
tokens: &'a [Token],
at: usize,
}
impl<'a> Cursor<'a> {
#[must_use]
pub fn new(tokens: &'a [Token]) -> Self {
assert!(
tokens.last().is_some_and(|token| token.is_eof()),
"the token stream must end in `Eof`"
);
Cursor { tokens, at: 0 }
}
#[inline]
#[must_use]
pub fn current(&self) -> Token {
self.tokens[self.at]
}
#[inline]
#[must_use]
pub fn peek(&self, n: usize) -> Token {
assert!(n <= MAX_LOOKAHEAD, "lookahead of {n} tokens, past the bound of {MAX_LOOKAHEAD}");
self.tokens[(self.at + n).min(self.tokens.len() - 1)]
}
#[inline]
#[must_use]
pub fn span(&self) -> Span {
self.current().span
}
#[must_use]
pub fn prev_end(&self) -> Span {
match self.at.checked_sub(1) {
Some(prev) => Span::empty_at(self.tokens[prev].span.hi),
None => Span::empty_at(self.current().span.lo),
}
}
#[inline]
#[must_use]
pub fn is_eof(&self) -> bool {
self.current().is_eof()
}
#[inline]
#[must_use]
pub fn at(&self, kind: TokenKind) -> bool {
self.current().kind == kind
}
#[inline]
#[must_use]
pub fn at_punct(&self, punct: Punct) -> bool {
self.current().punct() == Some(punct)
}
#[inline]
#[must_use]
pub fn at_keyword(&self, keyword: Keyword) -> bool {
self.current().keyword() == Some(keyword)
}
#[inline]
pub fn bump(&mut self) -> Token {
let token = self.current();
if !token.is_eof() {
self.at += 1;
}
token
}
#[inline]
pub fn eat(&mut self, kind: TokenKind) -> bool {
let matched = self.at(kind);
if matched {
self.bump();
}
matched
}
#[inline]
pub fn eat_punct(&mut self, punct: Punct) -> bool {
self.eat(TokenKind::Punct(punct))
}
#[inline]
pub fn eat_keyword(&mut self, keyword: Keyword) -> bool {
self.eat(TokenKind::Keyword(keyword))
}
#[inline]
#[must_use]
pub fn index(&self) -> usize {
self.at
}
#[inline]
#[must_use]
pub fn save(&self) -> Mark {
Mark(self.at)
}
#[inline]
pub fn restore(&mut self, mark: Mark) {
assert!(mark.0 < self.tokens.len(), "restoring a mark from another token stream");
self.at = mark.0;
}
}
#[cfg(test)]
mod tests {
use rucc_lex::TokenFlags;
use super::*;
fn stream(puncts: &[Punct]) -> Vec<Token> {
let mut tokens: Vec<Token> = puncts
.iter()
.enumerate()
.map(|(i, &punct)| Token {
kind: TokenKind::Punct(punct),
flags: TokenFlags::EMPTY,
value: 0,
span: Span::new(i as u32, i as u32 + 1),
})
.collect();
let end = puncts.len() as u32;
tokens.push(Token {
kind: TokenKind::Eof,
flags: TokenFlags::EMPTY,
value: 0,
span: Span::empty_at(end),
});
tokens
}
#[test]
fn peeking_past_the_end_gives_eof() {
let tokens = stream(&[Punct::Semi]);
let cursor = Cursor::new(&tokens);
assert!(cursor.peek(0).punct() == Some(Punct::Semi));
assert!(cursor.peek(1).is_eof());
assert!(cursor.peek(MAX_LOOKAHEAD).is_eof());
}
#[test]
fn bumping_stops_on_the_end() {
let tokens = stream(&[Punct::Semi]);
let mut cursor = Cursor::new(&tokens);
assert!(cursor.bump().punct() == Some(Punct::Semi));
for _ in 0..3 {
assert!(cursor.bump().is_eof());
}
assert_eq!(cursor.index(), 1);
}
#[test]
fn eating_only_moves_when_it_matches() {
let tokens = stream(&[Punct::Semi, Punct::Comma]);
let mut cursor = Cursor::new(&tokens);
assert!(!cursor.eat_punct(Punct::Comma));
assert_eq!(cursor.index(), 0);
assert!(cursor.eat_punct(Punct::Semi));
assert!(cursor.at_punct(Punct::Comma));
assert!(!cursor.eat_keyword(Keyword::Int));
}
#[test]
fn restoring_puts_the_cursor_back() {
let tokens = stream(&[Punct::LParen, Punct::Star, Punct::RParen]);
let mut cursor = Cursor::new(&tokens);
let mark = cursor.save();
cursor.bump();
cursor.bump();
assert!(cursor.at_punct(Punct::RParen));
cursor.restore(mark);
assert!(cursor.at_punct(Punct::LParen));
}
#[test]
fn a_missing_token_belongs_after_the_one_before_it() {
let tokens = stream(&[Punct::LParen, Punct::RParen]);
let mut cursor = Cursor::new(&tokens);
assert_eq!(cursor.prev_end(), Span::empty_at(0));
cursor.bump();
assert_eq!(cursor.prev_end(), Span::empty_at(1));
}
#[test]
#[should_panic(expected = "past the bound")]
fn looking_too_far_ahead_is_a_bug() {
let tokens = stream(&[Punct::Semi]);
let _ = Cursor::new(&tokens).peek(MAX_LOOKAHEAD + 1);
}
#[test]
#[should_panic(expected = "must end in `Eof`")]
fn a_stream_without_an_end_is_rejected() {
let _ = Cursor::new(&[]);
}
}