use crate::Token;
use logos::Logos;
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub struct Spanned {
pub token: Token,
pub start: usize,
pub end: usize,
}
impl Spanned {
#[must_use]
pub fn new(token: Token, start: usize, end: usize) -> Self {
Self { token, start, end }
}
#[must_use]
pub fn len(&self) -> usize {
self.end - self.start
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn span(&self) -> (usize, usize) {
(self.start, self.end)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexErrorLocation {
pub start: usize,
pub end: usize,
pub text: String,
}
#[derive(Error, Debug)]
#[error("failed to lex source: {count} error(s) found")]
pub struct LexError {
source_code: NamedSource<String>,
pub errors: Vec<LexErrorLocation>,
count: usize,
}
impl LexError {
fn new(source: String, errors: Vec<LexErrorLocation>) -> Self {
let count = errors.len();
Self {
source_code: NamedSource::new("<input>", source),
errors,
count,
}
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
let source = self.source_code.inner().clone();
self.source_code = NamedSource::new(filename.into(), source);
self
}
}
impl Diagnostic for LexError {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new("sage::lexer::E001"))
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new("remove or replace invalid characters"))
}
fn source_code(&self) -> Option<&dyn SourceCode> {
Some(&self.source_code)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let labels = self.errors.iter().map(|e| {
LabeledSpan::new_with_span(
Some(format!("invalid token `{}`", e.text)),
(e.start, e.end - e.start),
)
});
Some(Box::new(labels))
}
}
#[derive(Debug)]
pub struct LexResult {
pub tokens: Vec<Spanned>,
pub source: Arc<str>,
}
impl LexResult {
#[must_use]
pub fn tokens(&self) -> &[Spanned] {
&self.tokens
}
#[must_use]
pub fn into_tokens(self) -> Vec<Spanned> {
self.tokens
}
}
pub fn lex(source: &str) -> Result<LexResult, LexError> {
let source_arc: Arc<str> = Arc::from(source);
let mut tokens = Vec::new();
let mut errors = Vec::new();
let lexer = Token::lexer(source);
for (result, span) in lexer.spanned() {
if let Ok(token) = result {
tokens.push(Spanned::new(token, span.start, span.end));
} else {
let text = source[span.start..span.end].to_string();
errors.push(LexErrorLocation {
start: span.start,
end: span.end,
text,
});
}
}
if errors.is_empty() {
Ok(LexResult {
tokens,
source: source_arc,
})
} else {
Err(LexError::new(source.to_string(), errors))
}
}
#[must_use]
pub fn lex_partial(source: &str) -> (Vec<Spanned>, Vec<LexErrorLocation>) {
let mut tokens = Vec::new();
let mut errors = Vec::new();
let lexer = Token::lexer(source);
for (result, span) in lexer.spanned() {
if let Ok(token) = result {
tokens.push(Spanned::new(token, span.start, span.end));
} else {
let text = source[span.start..span.end].to_string();
errors.push(LexErrorLocation {
start: span.start,
end: span.end,
text,
});
}
}
(tokens, errors)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lex_simple_tokens() {
let result = lex("let x = 42").unwrap();
let tokens = result.tokens();
assert_eq!(tokens.len(), 4);
assert_eq!(tokens[0].token, Token::KwLet);
assert_eq!(tokens[1].token, Token::Ident);
assert_eq!(tokens[2].token, Token::Eq);
assert_eq!(tokens[3].token, Token::IntLit);
}
#[test]
fn lex_preserves_spans() {
let result = lex("let x").unwrap();
let tokens = result.tokens();
assert_eq!(tokens[0].start, 0);
assert_eq!(tokens[0].end, 3); assert_eq!(tokens[1].start, 4);
assert_eq!(tokens[1].end, 5); }
#[test]
fn lex_empty_source() {
let result = lex("").unwrap();
assert!(result.tokens().is_empty());
}
#[test]
fn lex_whitespace_only() {
let result = lex(" \n\t ").unwrap();
assert!(result.tokens().is_empty());
}
#[test]
fn lex_comments_only() {
let result = lex("// this is a comment").unwrap();
assert!(result.tokens().is_empty());
}
#[test]
fn lex_error_invalid_char() {
let err = lex("let # = 42").unwrap_err();
assert_eq!(err.errors.len(), 1);
assert_eq!(err.errors[0].text, "#");
assert_eq!(err.errors[0].start, 4);
assert_eq!(err.errors[0].end, 5);
}
#[test]
fn lex_error_multiple_invalid() {
let err = lex("let # x $ y").unwrap_err();
assert_eq!(err.errors.len(), 2);
assert_eq!(err.errors[0].text, "#");
assert_eq!(err.errors[1].text, "$");
}
#[test]
fn lex_partial_with_errors() {
let (tokens, errors) = lex_partial("let # x = 42");
assert_eq!(tokens.len(), 4); assert_eq!(tokens[0].token, Token::KwLet);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].text, "#");
}
#[test]
fn lex_agent_declaration() {
let source = r#"
agent Researcher {
belief topic: String
on start {
let result = infer("test")
emit(result)
}
}
run Researcher
"#;
let result = lex(source).unwrap();
let tokens = result.tokens();
assert_eq!(tokens[0].token, Token::KwAgent);
assert_eq!(tokens[1].token, Token::Ident);
assert_eq!(tokens[2].token, Token::LBrace);
assert_eq!(tokens[3].token, Token::KwBelief);
}
#[test]
fn lex_result_into_tokens() {
let result = lex("42").unwrap();
let tokens = result.into_tokens();
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token, Token::IntLit);
}
#[test]
fn spanned_len() {
let spanned = Spanned::new(Token::KwLet, 0, 3);
assert_eq!(spanned.len(), 3);
assert!(!spanned.is_empty());
}
#[test]
fn spanned_span() {
let spanned = Spanned::new(Token::KwLet, 5, 8);
assert_eq!(spanned.span(), (5, 8));
}
#[test]
fn lex_error_display() {
let err = lex("#").unwrap_err();
let display = format!("{err}");
assert!(display.contains("failed to lex"));
}
#[test]
fn lex_string_interpolation_markers() {
let result = lex(r#""Hello {name}!""#).unwrap();
let tokens = result.tokens();
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token, Token::StringLit);
}
#[test]
fn lex_complex_expression() {
let result = lex("a + b * c == d && e || !f").unwrap();
let tokens = result.tokens();
assert_eq!(tokens.len(), 12);
assert_eq!(tokens[0].token, Token::Ident); assert_eq!(tokens[1].token, Token::Plus); assert_eq!(tokens[2].token, Token::Ident); assert_eq!(tokens[3].token, Token::Star); assert_eq!(tokens[4].token, Token::Ident); assert_eq!(tokens[5].token, Token::EqEq); assert_eq!(tokens[6].token, Token::Ident); assert_eq!(tokens[7].token, Token::And); assert_eq!(tokens[8].token, Token::Ident); assert_eq!(tokens[9].token, Token::Or); assert_eq!(tokens[10].token, Token::Bang); assert_eq!(tokens[11].token, Token::Ident); }
#[test]
fn lex_list_type() {
let result = lex("List<String>").unwrap();
let tokens = result.tokens();
assert_eq!(tokens.len(), 4);
assert_eq!(tokens[0].token, Token::TyList);
assert_eq!(tokens[1].token, Token::Lt);
assert_eq!(tokens[2].token, Token::TyString);
assert_eq!(tokens[3].token, Token::Gt);
}
#[test]
fn lex_error_with_filename() {
let err = lex("#").unwrap_err().with_filename("test.sg");
assert_eq!(err.errors.len(), 1);
}
#[test]
fn lex_error_is_diagnostic() {
use miette::Diagnostic;
let err = lex("#").unwrap_err();
assert!(err.code().is_some());
assert!(err.help().is_some());
assert!(err.source_code().is_some());
assert!(err.labels().is_some());
}
}