use memchr::{memchr, memchr_iter, memrchr};
use molframe_core::span::{ByteSpan, Position};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Quoting {
Bare,
Single,
Double,
Text,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Token<'a> {
Block(&'a str),
FrameStart(&'a str),
FrameEnd,
Loop,
Tag(&'a str),
Value(&'a str, Quoting),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Spanned<'a> {
pub token: Token<'a>,
pub span: ByteSpan,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LexError {
UnterminatedQuote(Position),
UnterminatedText(Position),
NotText,
PositionOverflow(Position),
}
#[derive(Clone, Debug)]
pub struct Lexer<'a> {
input: &'a str,
at: Position,
}
impl<'a> Lexer<'a> {
pub fn new(bytes: &'a [u8]) -> Result<Self, LexError> {
let input = str::from_utf8(bytes).map_err(|_| LexError::NotText)?;
Ok(Self {
input,
at: Position::START,
})
}
#[must_use]
pub const fn position(&self) -> Position {
self.at
}
pub fn next_token(&mut self) -> Result<Option<Spanned<'a>>, LexError> {
self.skip_trivia()?;
let start = self.at;
let Some(byte) = self.peek() else {
return Ok(None);
};
let token = match byte {
b'_' => Token::Tag(self.take_bare()?),
b'\'' => Token::Value(self.take_quoted(b'\'')?, Quoting::Single),
b'"' => Token::Value(self.take_quoted(b'"')?, Quoting::Double),
b';' if start.column == 1 => Token::Value(self.take_text()?, Quoting::Text),
_ => self.keyword_or_value()?,
};
Ok(Some(Spanned {
token,
span: ByteSpan::new(start, self.at.byte_offset),
}))
}
fn keyword_or_value(&mut self) -> Result<Token<'a>, LexError> {
let word = self.take_bare()?;
match word.as_bytes().first() {
Some(b'd' | b'D') => {
if let Some(name) = strip_prefix_ignore_case(word, "data_") {
return Ok(Token::Block(name));
}
}
Some(b's' | b'S') => {
if let Some(name) = strip_prefix_ignore_case(word, "save_") {
return Ok(if name.is_empty() {
Token::FrameEnd
} else {
Token::FrameStart(name)
});
}
}
Some(b'l' | b'L') if word.eq_ignore_ascii_case("loop_") => {
return Ok(Token::Loop);
}
_ => {}
}
Ok(Token::Value(word, Quoting::Bare))
}
fn peek(&self) -> Option<u8> {
self.remaining().as_bytes().first().copied()
}
fn remaining(&self) -> &'a str {
match usize::try_from(self.at.byte_offset) {
Ok(offset) => slice_from(self.input, offset),
Err(_) => "",
}
}
fn advance(&mut self, count: usize) -> Result<(), LexError> {
const BULK_THRESHOLD: usize = 32;
let text = self.remaining();
let skipped = match text.as_bytes().get(..count) {
Some(skipped) => skipped,
None => text.as_bytes(),
};
if skipped.len() < BULK_THRESHOLD {
return self.advance_bytewise(skipped);
}
self.advance_all(skipped)
}
fn advance_bytewise(&mut self, skipped: &[u8]) -> Result<(), LexError> {
for byte in skipped {
self.at = self
.at
.advance(*byte)
.ok_or(LexError::PositionOverflow(self.at))?;
}
Ok(())
}
fn advance_all(&mut self, skipped: &[u8]) -> Result<(), LexError> {
let Ok(length) = u64::try_from(skipped.len()) else {
return Err(LexError::PositionOverflow(self.at));
};
let Some(byte_offset) = self.at.byte_offset.checked_add(length) else {
return Err(LexError::PositionOverflow(self.at));
};
let newlines = memchr_iter(b'\n', skipped).count();
if newlines == 0 {
let Ok(narrow) = u64::try_from(skipped.len()) else {
return Err(LexError::PositionOverflow(self.at));
};
let Some(column) = self.at.column.checked_add(narrow) else {
return Err(LexError::PositionOverflow(self.at));
};
self.at.byte_offset = byte_offset;
self.at.column = column;
return Ok(());
}
let Ok(newlines) = u64::try_from(newlines) else {
return Err(LexError::PositionOverflow(self.at));
};
let Some(line) = self.at.line.checked_add(newlines) else {
return Err(LexError::PositionOverflow(self.at));
};
let Some(last) = memrchr(b'\n', skipped) else {
return Err(LexError::PositionOverflow(self.at));
};
let Ok(after) = u64::try_from(skipped.len() - last - 1) else {
return Err(LexError::PositionOverflow(self.at));
};
let Some(column) = after.checked_add(1) else {
return Err(LexError::PositionOverflow(self.at));
};
self.at.byte_offset = byte_offset;
self.at.line = line;
self.at.column = column;
Ok(())
}
fn advance_non_newline(&mut self, count: usize) -> Result<(), LexError> {
let Ok(narrow) = u64::try_from(count) else {
return self.advance(count);
};
let Some(byte_offset) = self.at.byte_offset.checked_add(narrow) else {
return self.advance(count);
};
let Some(column) = self.at.column.checked_add(narrow) else {
return self.advance(count);
};
self.at.byte_offset = byte_offset;
self.at.column = column;
Ok(())
}
fn skip_trivia(&mut self) -> Result<(), LexError> {
loop {
let text = self.remaining();
let bytes = text.as_bytes();
let spaces = match bytes.iter().position(|byte| !byte.is_ascii_whitespace()) {
Some(spaces) => spaces,
None => bytes.len(),
};
match bytes.get(..spaces) {
Some(run) => self.advance_all(run)?,
None => self.advance(spaces)?,
}
if self.peek() != Some(b'#') {
return Ok(());
}
let rest = self.remaining();
let line = match memchr(b'\n', rest.as_bytes()) {
Some(line) => line,
None => rest.len(),
};
self.advance(line)?;
}
}
fn take_bare(&mut self) -> Result<&'a str, LexError> {
let text = self.remaining();
let end = text
.bytes()
.take_while(|byte| !byte.is_ascii_whitespace())
.count();
self.advance_non_newline(end)?;
Ok(slice_to(text, end))
}
fn take_quoted(&mut self, quote: u8) -> Result<&'a str, LexError> {
let opened_at = self.at;
self.advance(1)?;
let text = self.remaining();
let bytes = text.as_bytes();
let mut cursor = 0usize;
while cursor < bytes.len() {
let Some(offset) = memchr(quote, bytes_from(bytes, cursor)) else {
break;
};
let candidate = cursor + offset;
let closes = match bytes.get(candidate + 1) {
None => true,
Some(next) => next.is_ascii_whitespace(),
};
if closes {
self.advance(candidate + 1)?;
return Ok(slice_to(text, candidate));
}
if bytes.get(candidate).is_some_and(|byte| *byte == b'\n') {
break;
}
cursor = candidate + 1;
}
Err(LexError::UnterminatedQuote(opened_at))
}
fn take_text(&mut self) -> Result<&'a str, LexError> {
let opened_at = self.at;
self.advance(1)?;
let text = self.remaining();
let bytes = text.as_bytes();
let mut cursor = 0usize;
loop {
let Some(offset) = memchr(b'\n', bytes_from(bytes, cursor)) else {
return Err(LexError::UnterminatedText(opened_at));
};
let line_start = cursor + offset + 1;
if bytes.get(line_start) == Some(&b';') {
let value = slice_to(text, cursor + offset);
self.advance(line_start + 1)?;
return Ok(value.trim_start_matches(['\r', '\n']));
}
cursor = line_start;
}
}
}
fn slice_from(text: &str, start: usize) -> &str {
let Some(slice) = text.get(start..) else {
return "";
};
slice
}
fn slice_to(text: &str, end: usize) -> &str {
let Some(slice) = text.get(..end) else {
return "";
};
slice
}
fn bytes_from(bytes: &[u8], start: usize) -> &[u8] {
match bytes.get(start..) {
Some(slice) => slice,
None => &[],
}
}
fn strip_prefix_ignore_case<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
let head = text.get(..prefix.len())?;
head.eq_ignore_ascii_case(prefix)
.then(|| text.get(prefix.len()..))?
}
#[cfg(test)]
#[path = "tokenize_tests.rs"]
mod tests;