use core::str::Chars;
use intern_lang::{Interner, Symbol};
use source_lang::SourceFile;
use span_lang::{BytePos, Span};
use token_lang::Token;
#[derive(Clone, Debug)]
pub struct Cursor<'a> {
text: &'a str,
chars: Chars<'a>,
token_start: u32,
base: u32,
}
impl<'a> Cursor<'a> {
#[inline]
#[must_use]
pub fn new(text: &'a str) -> Self {
Self::with_base(text, 0)
}
#[inline]
#[must_use]
pub fn with_base(text: &'a str, base: u32) -> Self {
Self {
text,
chars: text.chars(),
token_start: 0,
base,
}
}
#[inline]
#[must_use]
pub fn for_source(file: &'a SourceFile) -> Self {
Self::with_base(file.text(), file.span().start().to_u32())
}
#[inline]
#[must_use]
pub fn remaining(&self) -> &'a str {
self.chars.as_str()
}
#[inline]
#[must_use]
pub fn is_eof(&self) -> bool {
self.chars.as_str().is_empty()
}
#[inline]
fn offset(&self) -> u32 {
(self.text.len() - self.chars.as_str().len()) as u32
}
#[inline]
#[must_use]
pub fn pos(&self) -> BytePos {
BytePos::new(self.base.saturating_add(self.offset()))
}
#[inline]
#[must_use]
pub fn first(&self) -> Option<char> {
self.chars.clone().next()
}
#[inline]
#[must_use]
pub fn second(&self) -> Option<char> {
let mut chars = self.chars.clone();
let _ = chars.next();
chars.next()
}
#[inline]
pub fn bump(&mut self) -> Option<char> {
self.chars.next()
}
#[inline]
pub fn bump_if(&mut self, expected: char) -> bool {
if self.first() == Some(expected) {
let _ = self.bump();
true
} else {
false
}
}
#[inline]
pub fn eat_while(&mut self, mut pred: impl FnMut(char) -> bool) {
while let Some(c) = self.first() {
if !pred(c) {
break;
}
let _ = self.bump();
}
}
#[inline]
#[must_use]
pub fn lexeme(&self) -> &'a str {
&self.text[self.token_start as usize..self.offset() as usize]
}
#[inline]
#[must_use]
pub fn token_span(&self) -> Span {
Span::new(
self.base.saturating_add(self.token_start),
self.base.saturating_add(self.offset()),
)
}
#[inline]
pub fn intern_lexeme(&self, interner: &mut Interner) -> Symbol {
interner.intern(self.lexeme())
}
#[inline]
pub fn emit<K>(&mut self, kind: K) -> Token<K> {
let token = Token::new(kind, self.token_span());
self.token_start = self.offset();
token
}
#[inline]
pub fn reset_token(&mut self) {
self.token_start = self.offset();
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, unused_results)]
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use super::*;
#[test]
fn test_bump_walks_every_char_then_eof() {
let mut cursor = Cursor::new("abc");
assert_eq!(cursor.bump(), Some('a'));
assert_eq!(cursor.bump(), Some('b'));
assert_eq!(cursor.bump(), Some('c'));
assert_eq!(cursor.bump(), None);
assert!(cursor.is_eof());
}
#[test]
fn test_first_and_second_peek_without_consuming() {
let cursor = Cursor::new("=>x");
assert_eq!(cursor.first(), Some('='));
assert_eq!(cursor.second(), Some('>'));
assert_eq!(cursor.pos(), BytePos::new(0));
}
#[test]
fn test_second_is_none_past_end() {
let cursor = Cursor::new("a");
assert_eq!(cursor.first(), Some('a'));
assert_eq!(cursor.second(), None);
}
#[test]
fn test_bump_if_only_consumes_on_match() {
let mut cursor = Cursor::new("=>");
assert!(!cursor.bump_if('!'));
assert_eq!(cursor.pos(), BytePos::new(0));
assert!(cursor.bump_if('='));
assert_eq!(cursor.pos(), BytePos::new(1));
}
#[test]
fn test_eat_while_stops_at_predicate_boundary() {
let mut cursor = Cursor::new("123abc");
cursor.eat_while(|c| c.is_ascii_digit());
assert_eq!(cursor.lexeme(), "123");
assert_eq!(cursor.remaining(), "abc");
}
#[test]
fn test_lexeme_and_span_track_the_current_run() {
let mut cursor = Cursor::with_base("let x", 5);
cursor.eat_while(|c| c.is_ascii_alphabetic());
assert_eq!(cursor.lexeme(), "let");
assert_eq!(cursor.token_span(), Span::new(5, 8));
}
#[test]
fn test_emit_resets_the_token_start() {
let mut cursor = Cursor::new("ab");
cursor.bump();
assert_eq!(cursor.emit("a"), Token::new("a", Span::new(0, 1)));
assert_eq!(cursor.lexeme(), "");
assert_eq!(cursor.token_span(), Span::new(1, 1));
cursor.bump();
assert_eq!(cursor.emit("b"), Token::new("b", Span::new(1, 2)));
}
#[test]
fn test_emit_at_eof_is_an_empty_span() {
let mut cursor = Cursor::new("a");
cursor.bump();
let _ = cursor.emit("a");
let eof = cursor.emit("eof");
assert_eq!(eof, Token::new("eof", Span::new(1, 1)));
assert!(eof.span().is_empty());
}
#[test]
fn test_reset_token_drops_the_run_without_emitting() {
let mut cursor = Cursor::new(" x");
cursor.eat_while(char::is_whitespace);
cursor.reset_token();
cursor.eat_while(|c| c.is_ascii_alphabetic());
assert_eq!(cursor.lexeme(), "x");
assert_eq!(cursor.token_span(), Span::new(2, 3));
}
#[test]
fn test_reset_token_then_emit_spans_only_the_kept_run() {
let mut cursor = Cursor::new("// c\nv");
cursor.eat_while(|c| c != '\n'); cursor.reset_token();
cursor.bump(); cursor.reset_token();
cursor.bump(); assert_eq!(cursor.emit("ident"), Token::new("ident", Span::new(5, 6)));
}
#[test]
fn test_positions_saturate_instead_of_wrapping() {
let mut cursor = Cursor::with_base("ab", u32::MAX - 1);
assert_eq!(cursor.pos(), BytePos::new(u32::MAX - 1));
cursor.bump();
assert_eq!(cursor.pos(), BytePos::new(u32::MAX));
cursor.bump();
assert_eq!(cursor.pos(), BytePos::new(u32::MAX)); }
#[test]
fn test_multibyte_chars_advance_by_byte_length() {
let mut cursor = Cursor::new("αβ");
assert_eq!(cursor.bump(), Some('α'));
assert_eq!(cursor.pos(), BytePos::new(2));
assert_eq!(cursor.bump(), Some('β'));
assert_eq!(cursor.pos(), BytePos::new(4));
assert!(cursor.is_eof());
}
#[test]
fn test_lexeme_slices_on_char_boundaries() {
let mut cursor = Cursor::new("αβγ");
cursor.bump();
cursor.bump();
assert_eq!(cursor.lexeme(), "αβ");
}
#[test]
fn test_for_source_offsets_spans_into_global_space() {
use source_lang::SourceMap;
let mut map = SourceMap::new();
map.add("a", "first").expect("fits");
let id = map.add("b", "xy").expect("fits");
let file = map.source(id).expect("just added");
let mut cursor = Cursor::for_source(file);
assert_eq!(cursor.pos().to_u32(), 5);
cursor.eat_while(|_| true);
assert_eq!(cursor.token_span(), Span::new(5, 7));
}
#[test]
fn test_intern_lexeme_interns_the_current_run() {
let mut interner = Interner::new();
let mut cursor = Cursor::new("foo bar");
cursor.eat_while(|c| c.is_ascii_alphabetic());
let foo = cursor.intern_lexeme(&mut interner);
assert_eq!(interner.resolve(foo), Some("foo"));
}
#[test]
fn test_full_lex_tiles_the_source() {
let src = "ab cd";
let mut cursor = Cursor::new(src);
let mut spans: Vec<Span> = Vec::new();
let mut lexemes = String::new();
while !cursor.is_eof() {
let c = cursor.first().unwrap();
if c.is_whitespace() {
cursor.eat_while(char::is_whitespace);
} else {
cursor.eat_while(|c| !c.is_whitespace());
}
let tok = cursor.emit(());
lexemes.push_str(&src[tok.span().start().to_usize()..tok.span().end().to_usize()]);
spans.push(tok.span());
}
assert_eq!(spans.first().unwrap().start(), BytePos::new(0));
assert_eq!(spans.last().unwrap().end(), BytePos::new(src.len() as u32));
for pair in spans.windows(2) {
assert_eq!(pair[0].end(), pair[1].start());
}
assert_eq!(lexemes, src);
}
}