use std::ops::Range;
use logos::Logos;
#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[logos(skip r"[ \t\r\n]+")]
#[logos(skip r"/\*([^*]|\*[^/])*\*/")]
pub enum Kind {
#[regex(r"\|[^|]*\|")]
Term,
#[regex(r#""([^"\\]|\\.)*""#)]
String,
#[regex(r"[A-Za-z][A-Za-z0-9\-]*#[A-Za-z0-9\-._]+")]
AltIdentifier,
#[regex(r"[0-9]+")]
Integer,
#[regex(r"[A-Za-z][A-Za-z0-9_\-]*")]
Identifier,
#[token("<<!")]
ChildOrSelfOf,
#[token("<<")]
DescendantOrSelfOf,
#[token("<!")]
ChildOf,
#[token("<=")]
LessOrEqual,
#[token("<")]
LessThan,
#[token(">>!")]
ParentOrSelfOf,
#[token(">>")]
AncestorOrSelfOf,
#[token(">!")]
ParentOf,
#[token(">=")]
GreaterOrEqual,
#[token(">")]
GreaterThan,
#[token("!!>")]
Top,
#[token("!!<")]
Bottom,
#[token("!=")]
NotEqual,
#[token("=")]
Equal,
#[token("(")]
LeftParen,
#[token(")")]
RightParen,
#[token("{{")]
DoubleLeftBrace,
#[token("}}")]
DoubleRightBrace,
#[token("{")]
LeftBrace,
#[token("}")]
RightBrace,
#[token("[")]
LeftBracket,
#[token("]")]
RightBracket,
#[token(":")]
Colon,
#[token(",")]
Comma,
#[token("^")]
Caret,
#[token("..")]
To,
#[token(".")]
Period,
#[token("*")]
Asterisk,
#[token("#")]
Hash,
#[token("+")]
Plus,
#[token("-")]
Dash,
}
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::AltIdentifier => "an alternate identifier",
Self::Integer => "a number",
Self::Identifier => "a word",
Self::ChildOrSelfOf => "'<<!'",
Self::DescendantOrSelfOf => "'<<'",
Self::ChildOf => "'<!'",
Self::LessOrEqual => "'<='",
Self::LessThan => "'<'",
Self::ParentOrSelfOf => "'>>!'",
Self::AncestorOrSelfOf => "'>>'",
Self::ParentOf => "'>!'",
Self::GreaterOrEqual => "'>='",
Self::GreaterThan => "'>'",
Self::Top => "'!!>'",
Self::Bottom => "'!!<'",
Self::NotEqual => "'!='",
Self::Equal => "'='",
Self::LeftParen => "'('",
Self::RightParen => "')'",
Self::DoubleLeftBrace => "'{{'",
Self::DoubleRightBrace => "'}}'",
Self::LeftBrace => "'{'",
Self::RightBrace => "'}'",
Self::LeftBracket => "'['",
Self::RightBracket => "']'",
Self::Colon => "':'",
Self::Comma => "','",
Self::Caret => "'^'",
Self::To => "'..'",
Self::Period => "'.'",
Self::Asterisk => "'*'",
Self::Hash => "'#'",
Self::Plus => "'+'",
Self::Dash => "'-'",
}
}
}
#[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 operators_take_the_longest_match_and_comments_are_skipped() {
assert_eq!(
kinds("<<! /* c */ 123 |a b| {{ D term = \"x\" }} [1..*] LOINC#54-6 !!>"),
[
Kind::ChildOrSelfOf,
Kind::Integer,
Kind::Term,
Kind::DoubleLeftBrace,
Kind::Identifier,
Kind::Identifier,
Kind::Equal,
Kind::String,
Kind::DoubleRightBrace,
Kind::LeftBracket,
Kind::Integer,
Kind::To,
Kind::Asterisk,
Kind::RightBracket,
Kind::AltIdentifier,
Kind::Top,
]
);
assert_eq!(
kinds("#-5.5"),
[
Kind::Hash,
Kind::Dash,
Kind::Integer,
Kind::Period,
Kind::Integer
]
);
let error = lex("< 123 |unterminated").expect_err("refused");
assert_eq!(error.offset, 6);
assert_eq!(error.found, '|');
assert_eq!(lex("a /* open").expect_err("refused").offset, 2);
}
}