#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Token {
Id,
Int,
Float,
String,
Dot,
Hash,
Star,
Plus,
Greater,
Tilde,
Whitespace,
Question,
BraceOpen,
BraceClose,
Dollar,
Pipe,
ParenOpen,
ParenClose,
Comma,
Colon,
Semi,
Less,
BracketOpen,
BracketClose,
Comment,
Eof,
Unknown,
}
mod statics {
use super::Token;
use regex::{Regex, RegexSet};
use std::sync::LazyLock;
macro_rules! make_regex_set {
{$vis: vis ($tokens: ident, $re_set: ident, $re_compiled: ident) = {$($tk: ident <- $pat: literal)*};} => {
$vis static $tokens: &[Token] = &[
$(Token::$tk, )*
];
$vis static $re_set: LazyLock<RegexSet> = LazyLock::new(|| RegexSet::new(&[
$(
concat!("^", $pat),
)*
]).expect("error building RegexSet"));
$vis static $re_compiled: LazyLock<Vec<Regex>> = LazyLock::new(|| vec![
$(
Regex::new(concat!("^", $pat)).expect(concat!("Error building Regex `", $pat, "`")),
)*
]);
};
}
make_regex_set! {
pub(super) (TOKENS, REGEX_SET, REGEX_LIST) = {
Id <- "[a-zA-Z][a-zA-Z0-9_-]*"
Int <- "[+-]?[0-9]+"
Float <- r"[+-]?[0-9]+\.[0-9]*"
String <- r#""(\\.|[^\\"])*""#
Dot <- r"\."
Hash <- "#"
Star <- r"\*"
Plus <- r"\+"
Greater <- ">"
Tilde <- "~"
Whitespace <- r"\p{White_Space}+"
Question <- r"\?"
BraceOpen <- r"\{"
BraceClose <- r"\}"
Dollar <- r"\$"
Pipe <- r"\|"
ParenOpen <- r"\("
ParenClose <- r"\)"
Comma <- ","
Colon <- ":"
Semi <- ";"
Less <- "<"
BracketOpen <- r"\["
BracketClose <- r"\]"
Comment <- r"//[^\n]*"
};
}
}
#[derive(Debug, Clone)]
pub struct Scanner<'a> {
slice: &'a str,
idx: usize,
line: usize,
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct Span {
pub line: usize,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Lexeme<'a> {
pub token: Token,
pub value: &'a str,
}
const EOF: Lexeme = Lexeme {
token: Token::Eof,
value: "",
};
impl<'a> Scanner<'a> {
#[must_use]
pub const fn new(slice: &'a str) -> Self {
Self {
slice,
idx: 0,
line: 1,
}
}
#[must_use]
pub fn peek_token(&self) -> (Span, Lexeme<'a>) {
if self.idx >= self.slice.len() {
return (Span::default(), EOF);
}
statics::REGEX_SET
.matches(&self.slice[self.idx..])
.into_iter()
.map(|x| Lexeme {
token: statics::TOKENS[x],
value: statics::REGEX_LIST[x]
.find(&self.slice[self.idx..])
.expect("matched in set should match in list")
.as_str(),
})
.max_by_key(|x| x.value.len())
.map(|lx| {
(
Span {
line: self.line,
start: self.idx,
end: self.idx + lx.value.len(),
},
lx,
)
})
.unwrap_or((
Span {
line: self.line,
start: self.idx,
end: self.idx + 1,
},
Lexeme {
token: Token::Unknown,
value: &self.slice[self.idx..=self.idx],
},
))
}
pub fn eat_token(&mut self) -> (Span, Lexeme<'a>) {
let (span, lexeme) = self.peek_token();
self.idx += lexeme.value.len();
self.line += lexeme.value.chars().filter(|&x| x == '\n').count();
(span, lexeme)
}
pub fn peek_non_comment(&mut self) -> (Span, Lexeme<'a>) {
while let (
_,
Lexeme {
token: Token::Comment,
..
},
) = self.peek_token()
{
self.eat_token();
}
self.peek_token()
}
pub fn peek_non_whitespace(&mut self) -> (Span, Lexeme<'a>) {
while let (
_,
Lexeme {
token: Token::Whitespace,
..
},
) = self.peek_non_comment()
{
self.eat_token();
}
self.peek_token()
}
}
#[cfg(test)]
mod tests {
use super::{
statics::{REGEX_LIST, REGEX_SET},
Lexeme, Scanner, Token, EOF,
};
#[test]
fn test_tokens() {
let scanner = Scanner::new("");
assert_eq!(scanner.peek_token().1, EOF);
macro_rules! test_matches {
{$($tk: ident => $($pat: literal)+ $(!($($npat: literal)+))?)* } => {
$(
$(
assert_eq!(
Scanner::new($pat).peek_token().1,
Lexeme { token: Token::$tk, value: $pat }
);
)+
$(
$(
assert_ne!(
Scanner::new($npat).peek_token().1,
Lexeme { token: Token::$tk, value: $npat }
);
)*
)?
)*
};
}
test_matches! {
Id => "a" "a-" "A9-9-9-9" "a____a" !("9" "-" "_")
Int => "+1" "1" "1234" "-1" !("+" "-")
Float => "0." "-0.1234" "+0.12345" !("1" ".5" "-.5" ".")
String => r#""hello!""# r#""""# r#""\"""# !(r#"""""# r#""\""#)
Dot => "." !("a")
Star => "*"
Plus => "+"
Question => "?"
Pipe => "|"
BracketOpen => "["
BracketClose => "]"
}
}
macro_rules! lx {
($tk: ident, $lit: literal) => {
Lexeme {
token: Token::$tk,
value: $lit,
}
};
}
#[test]
fn test_eat() {
let mut sc = Scanner::new("h3 h4#h5.h6 {}");
assert_eq!(sc.eat_token().1, lx!(Id, "h3"));
assert_eq!(sc.eat_token().1, lx!(Whitespace, " "));
assert_eq!(sc.eat_token().1, lx!(Id, "h4"));
assert_eq!(sc.eat_token().1, lx!(Hash, "#"));
assert_eq!(sc.eat_token().1, lx!(Id, "h5"));
assert_eq!(sc.eat_token().1, lx!(Dot, "."));
assert_eq!(sc.eat_token().1, lx!(Id, "h6"));
assert_eq!(sc.eat_token().1, lx!(Whitespace, " "));
assert_eq!(sc.eat_token().1, lx!(BraceOpen, "{"));
assert_eq!(sc.eat_token().1, lx!(BraceClose, "}"));
}
#[test]
fn test_peek_whitespace() {
let mut sc = Scanner::new("h3 h4#h5.h6 {}");
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(Id, "h3"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(Id, "h4"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(Hash, "#"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(Id, "h5"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(Dot, "."));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(Id, "h6"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(BraceOpen, "{"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(BraceClose, "}"));
}
#[test]
fn test_whitespace_mix() {
let mut sc = Scanner::new("h3 h4#h5.h6 {}");
assert_eq!(sc.eat_token().1, lx!(Id, "h3"));
assert_eq!(sc.eat_token().1, lx!(Whitespace, " "));
assert_eq!(sc.eat_token().1, lx!(Id, "h4"));
assert_eq!(sc.eat_token().1, lx!(Hash, "#"));
assert_eq!(sc.eat_token().1, lx!(Id, "h5"));
assert_eq!(sc.eat_token().1, lx!(Dot, "."));
assert_eq!(sc.eat_token().1, lx!(Id, "h6"));
sc.peek_non_whitespace();
assert_eq!(sc.eat_token().1, lx!(BraceOpen, "{"));
assert_eq!(sc.eat_token().1, lx!(BraceClose, "}"));
}
#[test]
fn test_comments() {
let mut sc = Scanner::new(
r"// Hello! This is a comment!
b: a // and another! {
{
// } don't be fooled!
}",
);
assert_eq!(sc.peek_non_whitespace().1, lx!(Id, "b"));
sc.eat_token();
assert_eq!(sc.peek_non_whitespace().1, lx!(Colon, ":"));
sc.eat_token();
assert_eq!(sc.peek_non_whitespace().1, lx!(Id, "a"));
sc.eat_token();
assert_eq!(sc.peek_non_whitespace().1, lx!(BraceOpen, "{"));
sc.eat_token();
assert_eq!(sc.eat_token().1.token, Token::Whitespace);
assert_eq!(sc.eat_token().1, lx!(Comment, "// } don't be fooled!"));
assert_eq!(sc.peek_non_whitespace().1, lx!(BraceClose, "}"));
sc.eat_token();
assert_eq!(sc.eat_token().1.token, Token::Eof);
}
#[test]
fn all_regex_is_valid() {
let _ = &*REGEX_SET;
let _ = &*REGEX_LIST;
}
}