#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
Rule,
Match,
Require,
Rewrite,
Mod,
Ident,
Number,
LBrace,
RBrace,
LParen,
RParen,
LBracket,
RBracket,
Comma,
Newline,
EqEq,
Plus,
Minus,
Star,
Slash,
Eof,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Token<'a> {
pub kind: TokenKind,
pub text: &'a str,
pub pos: usize,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[error("lexer error at byte {pos}: {msg}")]
pub struct LexError {
pub msg: String,
pub pos: usize,
}
pub struct Lexer<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Lexer<'a> {
pub fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
pub fn next_token(&mut self) -> Result<Token<'a>, LexError> {
self.skip_horizontal_whitespace_and_comments();
if self.pos >= self.input.len() {
return Ok(Token {
kind: TokenKind::Eof,
text: "",
pos: self.pos,
});
}
let start = self.pos;
let ch = self.peek_char().unwrap();
match ch {
'\n' | '\r' => {
self.advance();
if ch == '\r' && self.peek_char() == Some('\n') {
self.advance();
}
loop {
self.skip_horizontal_whitespace_and_comments();
match self.peek_char() {
Some('\n') => {
self.advance();
}
Some('\r') => {
self.advance();
if self.peek_char() == Some('\n') {
self.advance();
}
}
_ => break,
}
}
self.make_token(TokenKind::Newline, start)
}
'{' => {
self.advance();
self.make_token(TokenKind::LBrace, start)
}
'}' => {
self.advance();
self.make_token(TokenKind::RBrace, start)
}
'(' => {
self.advance();
self.make_token(TokenKind::LParen, start)
}
')' => {
self.advance();
self.make_token(TokenKind::RParen, start)
}
'[' => {
self.advance();
self.make_token(TokenKind::LBracket, start)
}
']' => {
self.advance();
self.make_token(TokenKind::RBracket, start)
}
',' => {
self.advance();
self.make_token(TokenKind::Comma, start)
}
'+' => {
self.advance();
self.make_token(TokenKind::Plus, start)
}
'-' => {
self.advance();
self.make_token(TokenKind::Minus, start)
}
'*' => {
self.advance();
self.make_token(TokenKind::Star, start)
}
'/' => {
self.advance();
if self.peek_char() == Some('/') {
self.skip_line_comment();
self.next_token()
} else {
self.make_token(TokenKind::Slash, start)
}
}
'=' => {
self.advance();
if self.peek_char() == Some('=') {
self.advance();
self.make_token(TokenKind::EqEq, start)
} else {
Err(LexError {
msg: format!("unexpected character '{}'", ch),
pos: start,
})
}
}
_ if ch.is_alphabetic() || ch == '_' => self.read_ident(start),
_ if ch.is_ascii_digit() || ch == '.' => self.read_number(start),
_ => Err(LexError {
msg: format!("unexpected character '{}'", ch),
pos: start,
}),
}
}
fn read_ident(&mut self, start: usize) -> Result<Token<'a>, LexError> {
while let Some(ch) = self.peek_char() {
if ch.is_alphanumeric() || ch == '_' {
self.advance();
} else {
break;
}
}
let text = &self.input[start..self.pos];
let kind = match text {
"rule" => TokenKind::Rule,
"match" => TokenKind::Match,
"require" => TokenKind::Require,
"rewrite" => TokenKind::Rewrite,
"mod" => TokenKind::Mod,
_ => TokenKind::Ident,
};
Ok(Token {
kind,
text,
pos: start,
})
}
fn read_number(&mut self, start: usize) -> Result<Token<'a>, LexError> {
while let Some(ch) = self.peek_char() {
if ch.is_ascii_digit() || ch == '.' {
self.advance();
} else {
break;
}
}
let text = &self.input[start..self.pos];
Ok(Token {
kind: TokenKind::Number,
text,
pos: start,
})
}
fn skip_horizontal_whitespace_and_comments(&mut self) {
loop {
match self.peek_char() {
Some(ch) if ch.is_whitespace() && ch != '\n' && ch != '\r' => self.advance(),
Some('/') if self.peek_next() == Some('/') => self.skip_line_comment(),
_ => break,
}
}
}
fn skip_line_comment(&mut self) {
while let Some(ch) = self.peek_char() {
if ch == '\n' {
break;
}
self.advance();
}
}
fn make_token(&self, kind: TokenKind, start: usize) -> Result<Token<'a>, LexError> {
Ok(Token {
kind,
text: &self.input[start..self.pos],
pos: start,
})
}
fn peek_char(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn peek_next(&self) -> Option<char> {
self.input[self.pos..].chars().nth(1)
}
fn advance(&mut self) {
if let Some(ch) = self.peek_char() {
self.pos += ch.len_utf8();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lex_all(input: &str) -> Vec<TokenKind> {
let mut lexer = Lexer::new(input);
let mut kinds = Vec::new();
loop {
let token = lexer.next_token().unwrap();
kinds.push(token.kind.clone());
if token.kind == TokenKind::Eof {
break;
}
}
kinds
}
#[test]
fn lex_keywords_and_ident() {
let kinds = lex_all("rule match require rewrite mod foo");
assert_eq!(
kinds,
vec![
TokenKind::Rule,
TokenKind::Match,
TokenKind::Require,
TokenKind::Rewrite,
TokenKind::Mod,
TokenKind::Ident,
TokenKind::Eof,
]
);
}
#[test]
fn lex_punctuation() {
let kinds = lex_all("{ } ( ) [ ] , \n == + - * /");
assert_eq!(
kinds,
vec![
TokenKind::LBrace,
TokenKind::RBrace,
TokenKind::LParen,
TokenKind::RParen,
TokenKind::LBracket,
TokenKind::RBracket,
TokenKind::Comma,
TokenKind::Newline,
TokenKind::EqEq,
TokenKind::Plus,
TokenKind::Minus,
TokenKind::Star,
TokenKind::Slash,
TokenKind::Eof,
]
);
}
#[test]
fn lex_numbers() {
let kinds = lex_all("0 1 2.5 3.14");
assert_eq!(
kinds,
vec![
TokenKind::Number,
TokenKind::Number,
TokenKind::Number,
TokenKind::Number,
TokenKind::Eof,
]
);
}
#[test]
fn lex_comment() {
let kinds = lex_all("rule // this is a comment\nmatch");
assert_eq!(
kinds,
vec![
TokenKind::Rule,
TokenKind::Newline,
TokenKind::Match,
TokenKind::Eof
]
);
}
#[test]
fn lex_consecutive_newlines() {
let kinds = lex_all("rule\n\n\r\nmatch");
assert_eq!(
kinds,
vec![
TokenKind::Rule,
TokenKind::Newline,
TokenKind::Match,
TokenKind::Eof
]
);
}
#[test]
fn lex_unicode_identifier_accepted() {
let kinds = lex_all("rule α");
assert_eq!(
kinds,
vec![
TokenKind::Rule,
TokenKind::Ident, TokenKind::Eof
]
);
}
}