use molframe_core::diagnostic::{Code, Diagnostic};
#[derive(Clone, PartialEq, Eq, Debug)]
pub(super) struct Token {
pub(super) kind: TokenKind,
pub(super) offset: usize,
}
#[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 position = 0usize;
while position < bytes.len() {
if bytes[position].is_ascii_whitespace() {
position += 1;
continue;
}
let start = position;
match bytes[position] {
b'(' => {
tokens.push(Token {
kind: TokenKind::LeftParen,
offset: start,
});
position += 1;
}
b')' => {
tokens.push(Token {
kind: TokenKind::RightParen,
offset: start,
});
position += 1;
}
b'<' | b'>' | b'=' | b'!' => {
position += 1;
if position < bytes.len() && bytes[position] == b'=' {
position += 1;
}
tokens.push(Token {
kind: TokenKind::Operator(source[start..position].into()),
offset: start,
});
}
b'\'' | b'"' => {
let (value, next) = quoted_value(source, start)?;
tokens.push(Token {
kind: TokenKind::Value(value),
offset: start,
});
position = next;
}
_ => {
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(start, "selection contains an unreadable token"));
}
tokens.push(Token {
kind: TokenKind::Value(value.into()),
offset: start,
});
}
}
}
Ok(tokens)
}
fn quoted_value(source: &str, start: usize) -> Result<(Box<str>, usize), Diagnostic> {
let bytes = source.as_bytes();
let Some("e) = bytes.get(start) else {
return Err(syntax(start, "quoted selection value is not closed"));
};
let Some(content_start) = start.checked_add(1) else {
return Err(syntax(
start,
"quoted selection offset exceeds the addressable range",
));
};
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)?
};
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(syntax(start, "quoted selection value is not closed"))
}
fn decode_quoted(
source: &str,
start: usize,
end: usize,
escape_count: usize,
) -> Result<Box<str>, Diagnostic> {
let Some(capacity) = end
.checked_sub(start)
.and_then(|length| length.checked_sub(escape_count))
else {
return Err(syntax(start, "quoted selection bounds are inconsistent"));
};
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();
}
Ok(value.into_boxed_str())
}
fn syntax(offset: usize, message: &'static str) -> Diagnostic {
Diagnostic::new(Code::E4001)
.with_message(message)
.with_context("offset", offset.to_string())
}
#[cfg(test)]
#[path = "lexer_tests.rs"]
mod tests;