I have a lexer (program that turns source code into tokens) written in Go that I am re-writing in Rust.
The lexer should take in a sequence of tokens, generally different special characters, integers, or string sequences (identifiers) and output tokens representing those 'types'.
I'd like to write more idiomatic Rust, using non-mutable data wherever possible, but the current 'design' is akin to OOP with a struct mutating itself in most methods. I am struggling to come up with a better design that is more idiomatic Rust, so input would be highly appreciated.
As you can see in the code below, the Lexer
struct holds different position counters that are mutated in most methods, meaning that the entire struct is passed as mutable. This seems to be a specific code smell to me, and I would like to refactor that out if nothing else.
use crate::token::{match_keyword, Token};
/// A lexer contains a vector of characters as well as various pointers to read and peek characters
/// in the input.
pub struct Lexer {
input: Vec<char>,
position: usize, // current reading position
peek_position: usize, // current peeking position
ch: char, // current char under examination
}
impl Lexer {
pub fn new(input: String) -> Self {
let in_vec: Vec<char> = input.chars().collect();
let c = in_vec[0];
Self {
input: in_vec,
position: 0,
peek_position: 1,
ch: c,
}
}
pub fn next_token(&mut self) -> Option<Token> {
self.skip_whitespace();
let tok = match self.ch {
'+' => Token::PLUS,
'-' => Token::MINUS,
'*' => Token::ASTERISK,
'!' => {
if self.peek_char()? == '=' {
self.read_char(); // advance past '='
Token::NEQ
} else {
Token::BANG
}
}
'?' => Token::QUESTION,
'>' => Token::GT,
'<' => Token::LT,
',' => Token::COMMA,
';' => Token::SEMICOLON,
'(' => Token::LPAREN,
')' => Token::RPAREN,
'{' => Token::LBRACE,
'}' => Token::RBRACE,
'|' => {
if self.peek_char()? == '>' {
self.read_char();
Token::PIPE
} else if self.peek_char()? == '|' {
self.read_char();
Token::LOR
} else {
// FIXME is there a way around this?
Token::ILLEGAL(String::from(self.ch))
}
}
'/' => {
// TODO add comment support aka skip anything from double // to newline
if self.peek_char()? == '/' {
// need to skip to newline
while self.ch != '\n' {
self.read_char();
}
Token::COMMENT // this will be discarded in parsing. maybe keep it for reflection
} else {
Token::SLASH
}
}
'"' => {
while self.peek_char()? != '"' {
self.peek_position += 1;
}
self.position = self.peek_position; // catch current pos up to the peek
Token::STRING(
self.input[self.position..self.peek_position]
.into_iter()
.collect(),
)
}
_ => {
if self.ch.is_alphabetic() {
let mut lit = String::from(self.read_char());
while self.input.get(self.position)?.is_alphabetic() {
lit.push(self.read_char());
}
match_keyword(lit.as_str())
} else if self.ch.is_digit(10) {
let mut lit = String::from(self.read_char());
while self.input.get(self.position)?.is_digit(10) {
lit.push(self.read_char());
}
Token::INT(lit.parse::<i32>().unwrap()) // FIXME better error handling
} else {
Token::ILLEGAL(self.ch.to_string())
}
}
};
self.read_char();
Some(tok)
}
fn read_char(&mut self) -> char {
self.ch = *self.input.get(self.position).unwrap_or(&'\0'); // FIXME
self.position += 1;
self.ch
}
fn peek_char(&self) -> Option<char> {
// if self.peek_position >= self.input.len() {
// '\0'
// } else {
// self.input[l.readPosition]
// }
if let Some(ch) = self.input.get(self.peek_position) {
Some(*ch)
} else {
None
}
}
fn skip_whitespace(&mut self) {
while self.ch == ' ' || self.ch == '\t' || self.ch == '\n' || self.ch == '\r' {
self.read_char();
}
}
}