use std::ops::Range;
use logos::Logos;
#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[logos(skip r"[ \t\r\n]+")]
pub enum Kind {
#[regex(r"\|[^|]*\|")]
Term,
#[regex(r#""([^"\\]|\\"|\\\\)*""#)]
String,
#[regex(r"[0-9]+")]
Integer,
#[token("===")]
EquivalentTo,
#[token("<<<")]
SubtypeOf,
#[token("=")]
Equal,
#[token("+")]
Plus,
#[token(":")]
Colon,
#[token(",")]
Comma,
#[token("{")]
LeftBrace,
#[token("}")]
RightBrace,
#[token("(")]
LeftParen,
#[token(")")]
RightParen,
#[token("#")]
Hash,
#[token("-")]
Dash,
#[token(".")]
Period,
}
impl Kind {
#[must_use]
pub const fn describe(self) -> &'static str {
match self {
Self::Term => "a term between pipes",
Self::String => "a quoted string",
Self::Integer => "a number",
Self::EquivalentTo => "'==='",
Self::SubtypeOf => "'<<<'",
Self::Equal => "'='",
Self::Plus => "'+'",
Self::Colon => "':'",
Self::Comma => "','",
Self::LeftBrace => "'{'",
Self::RightBrace => "'}'",
Self::LeftParen => "'('",
Self::RightParen => "')'",
Self::Hash => "'#'",
Self::Dash => "'-'",
Self::Period => "'.'",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token<'s> {
pub kind: Kind,
pub text: &'s str,
pub span: Range<usize>,
}
impl PartialEq<Kind> for Token<'_> {
fn eq(&self, other: &Kind) -> bool {
self.kind == *other
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unexpected character {found:?} at byte {offset}")]
pub struct LexError {
pub offset: usize,
pub found: char,
}
pub fn lex(input: &str) -> Result<Vec<Token<'_>>, LexError> {
let mut lexer = Kind::lexer(input);
let mut tokens = Vec::new();
while let Some(kind) = lexer.next() {
let span = lexer.span();
let kind = kind.map_err(|()| LexError {
offset: span.start,
found: lexer.slice().chars().next().unwrap_or('\u{0}'),
})?;
tokens.push(Token {
kind,
text: lexer.slice(),
span,
});
}
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::{Kind, lex};
fn kinds(input: &str) -> Vec<Kind> {
lex(input)
.expect("lexes")
.into_iter()
.map(|t| t.kind)
.collect()
}
#[test]
fn the_definition_status_prefixes_take_the_longest_match() {
assert_eq!(kinds("=== 123456"), [Kind::EquivalentTo, Kind::Integer]);
assert_eq!(kinds("<<< 123456"), [Kind::SubtypeOf, Kind::Integer]);
}
#[test]
fn a_refinement_lexes_into_its_literals() {
assert_eq!(
kinds("1 |a b| : { 2 = (3 : 4 = #-5.5) , 6 = \"t\" }"),
[
Kind::Integer,
Kind::Term,
Kind::Colon,
Kind::LeftBrace,
Kind::Integer,
Kind::Equal,
Kind::LeftParen,
Kind::Integer,
Kind::Colon,
Kind::Integer,
Kind::Equal,
Kind::Hash,
Kind::Dash,
Kind::Integer,
Kind::Period,
Kind::Integer,
Kind::RightParen,
Kind::Comma,
Kind::Integer,
Kind::Equal,
Kind::String,
Kind::RightBrace,
]
);
}
#[test]
fn an_escape_the_grammar_does_not_define_does_not_continue_a_string() {
let error = lex(r#""a\nb" "#).expect_err("refused");
assert_eq!(error.offset, 0);
assert_eq!(error.found, '"');
}
#[test]
fn an_unterminated_term_or_string_is_refused_at_its_opening() {
assert_eq!(lex("123456 |unterminated").expect_err("refused").offset, 7);
assert_eq!(lex("123456 = \"open").expect_err("refused").offset, 9);
}
}