use molframe_core::diagnostic::{Code, Diagnostic};
use molframe_core::span::{ByteSpan, Position};
#[derive(Clone, PartialEq, Eq, Debug)]
pub(super) struct Token {
pub(super) kind: TokenKind,
pub(super) span: ByteSpan,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub(super) enum TokenKind {
Value(Box<str>),
Operator(Box<str>),
LeftParen,
RightParen,
}
pub(super) fn lex(source: &str) -> Result<Vec<Token>, Diagnostic> {
let bytes = source.as_bytes();
let mut tokens = Vec::new();
let mut cursor = Cursor::default();
let mut position = 0usize;
while position < bytes.len() {
if bytes[position].is_ascii_whitespace() {
cursor.consume(bytes, position, position + 1);
position += 1;
continue;
}
let start = position;
let kind = match bytes[position] {
b'(' => {
position += 1;
TokenKind::LeftParen
}
b')' => {
position += 1;
TokenKind::RightParen
}
b'<' | b'>' | b'=' | b'!' => {
position += 1;
if position < bytes.len() && bytes[position] == b'=' {
position += 1;
}
TokenKind::Operator(source[start..position].into())
}
b'\'' | b'"' => {
let (value, next) = quoted_value(source, start, cursor)?;
position = next;
TokenKind::Value(value)
}
_ => {
while position < bytes.len()
&& !bytes[position].is_ascii_whitespace()
&& !matches!(bytes[position], b'(' | b')' | b'<' | b'>' | b'=' | b'!')
{
position += 1;
}
let value = &source[start..position];
if value.is_empty() {
return Err(syntax(
cursor.span(start, start + 1),
"selection contains an unreadable token",
));
}
TokenKind::Value(value.into())
}
};
tokens.push(Token {
kind,
span: cursor.span(start, position),
});
cursor.consume(bytes, start, position);
}
Ok(tokens)
}
#[derive(Clone, Copy, Default)]
pub(super) struct Cursor {
line: u64,
line_start: usize,
}
impl Cursor {
pub(super) fn span(self, start: usize, end: usize) -> ByteSpan {
let column = start.saturating_sub(self.line_start);
ByteSpan::new(
Position::new(
widen(start),
self.line.saturating_add(1),
widen(column).saturating_add(1),
),
widen(end),
)
}
fn consume(&mut self, bytes: &[u8], start: usize, end: usize) {
let Some(slice) = bytes.get(start..end) else {
return;
};
for (index, byte) in slice.iter().enumerate() {
if *byte == b'\n' {
self.line = self.line.saturating_add(1);
self.line_start = start + index + 1;
}
}
}
}
pub(super) fn end_of(source: &str) -> ByteSpan {
let mut cursor = Cursor::default();
cursor.consume(source.as_bytes(), 0, source.len());
cursor.span(source.len(), source.len())
}
fn widen(value: usize) -> u64 {
value as u64
}
fn quoted_value(
source: &str,
start: usize,
cursor: Cursor,
) -> Result<(Box<str>, usize), Diagnostic> {
let bytes = source.as_bytes();
let unclosed = || {
syntax(
cursor.span(start, source.len()),
"quoted selection value is not closed",
)
};
let Some("e) = bytes.get(start) else {
return Err(unclosed());
};
let Some(content_start) = start.checked_add(1) else {
return Err(unclosed());
};
let mut position = content_start;
let mut escape_count = 0usize;
while position < bytes.len() {
if bytes[position] == quote {
let value = if escape_count == 0 {
source[content_start..position].into()
} else {
decode_quoted(source, content_start, position, escape_count).ok_or_else(|| {
syntax(
cursor.span(start, position + 1),
"quoted selection bounds are inconsistent",
)
})?
};
return Ok((value, position + 1));
}
if bytes[position] == b'\\' && position + 1 < bytes.len() {
escape_count += 1;
position += 1;
}
let Some(character) = source.get(position..).and_then(|tail| tail.chars().next()) else {
break;
};
position += character.len_utf8();
}
Err(unclosed())
}
fn decode_quoted(source: &str, start: usize, end: usize, escape_count: usize) -> Option<Box<str>> {
let capacity = end
.checked_sub(start)
.and_then(|length| length.checked_sub(escape_count))?;
let mut value = String::with_capacity(capacity);
let bytes = source.as_bytes();
let mut position = start;
while position < end {
if bytes.get(position) == Some(&b'\\') && position + 1 < end {
position += 1;
}
let Some(character) = source
.get(position..end)
.and_then(|tail| tail.chars().next())
else {
break;
};
value.push(character);
position += character.len_utf8();
}
Some(value.into_boxed_str())
}
pub(super) fn syntax(span: ByteSpan, message: &'static str) -> Diagnostic {
Diagnostic::new(Code::E4001).with_message(message).at(span)
}
#[cfg(test)]
#[path = "lexer_tests.rs"]
mod tests;