use alloc::{
collections::btree_map::BTreeMap,
string::{String, ToString},
};
use core::cmp::min;
use super::{
error::CompilationError,
token::{Token, TokenKind},
};
enum Started {
String,
Number, Ident,
IfEqualElse(TokenKind, TokenKind),
IfSameElse(TokenKind, TokenKind),
}
pub struct Lexer<'a> {
enums: &'a BTreeMap<String, i8>,
whole: &'a str,
rest: &'a str,
pos: usize,
line: usize,
}
impl<'a> Lexer<'a> {
#[must_use]
pub const fn new(enums: &'a BTreeMap<String, i8>, source_code: &'a str) -> Self {
Self {
enums,
whole: source_code,
rest: source_code,
pos: 0,
line: 1,
}
}
#[must_use]
pub const fn enums(&self) -> &BTreeMap<String, i8> {
self.enums
}
pub const fn set_input(&mut self, source_code: &'a str) {
self.whole = source_code;
self.rest = source_code;
self.pos = 0;
self.line = 1;
}
}
impl Iterator for Lexer<'_> {
type Item = Result<Token, CompilationError>;
#[allow(clippy::too_many_lines)]
fn next(&mut self) -> Option<Self::Item> {
loop {
let mut chars = self.rest.chars();
let c = chars.next()?;
let c_at = self.pos;
let c_len = c.len_utf8();
let c_str = &self.rest[..c_len];
let c_onwards = self.rest;
self.rest = chars.as_str();
self.pos += c_len;
let line = self.line;
let create = move |kind: TokenKind| {
Some(Ok(Token {
kind,
offset: c_at,
line,
origin: c_str.to_string(),
}))
};
let started = match c {
'(' => return create(TokenKind::LeftParen),
')' => return create(TokenKind::RightParen),
';' => return create(TokenKind::Semicolon),
'^' => return create(TokenKind::Caret),
'~' => return create(TokenKind::Tilde),
'?' => return create(TokenKind::QMark),
':' => Started::IfEqualElse(TokenKind::ColonEqual, TokenKind::Colon),
'=' => Started::IfEqualElse(TokenKind::EqualEqual, TokenKind::Equal),
'!' => Started::IfEqualElse(TokenKind::BangEqual, TokenKind::Bang),
'+' => Started::IfEqualElse(TokenKind::PlusEqual, TokenKind::Plus),
'-' => Started::IfEqualElse(TokenKind::MinusEqual, TokenKind::Minus),
'*' => Started::IfEqualElse(TokenKind::StarEqual, TokenKind::Star),
'/' => Started::IfEqualElse(TokenKind::SlashEqual, TokenKind::Slash),
'<' => Started::IfEqualElse(TokenKind::LessEqual, TokenKind::Less),
'>' => Started::IfEqualElse(TokenKind::GreaterEqual, TokenKind::Greater),
'&' => Started::IfSameElse(TokenKind::And, TokenKind::Ampersand),
'|' => Started::IfSameElse(TokenKind::Or, TokenKind::Pipe),
'\'' => Started::String,
'0'..='9' => Started::Number,
'a'..='z' | 'A'..='Z' | '_' | '@' => Started::Ident,
'\n' => {
self.line += 1;
continue;
}
c if c.is_whitespace() => continue,
c if c.is_alphabetic() => Started::Ident,
c => return Some(Err(CompilationError::UnexpectedChar { c, pos: self.line })),
};
break match started {
Started::IfEqualElse(yes, no) => {
self.rest = self.rest.trim_start();
let trimmed = c_onwards.len() - self.rest.len() - c_len;
self.pos += trimmed;
if self.rest.starts_with('=') {
let span = &c_onwards[..=c_len + trimmed];
self.rest = &self.rest[c_len..];
self.pos += c_len;
Some(Ok(Token {
origin: span.to_string(),
offset: c_at,
line: self.line,
kind: yes,
}))
} else {
Some(Ok(Token {
origin: c_str.to_string(),
offset: c_at,
line: self.line,
kind: no,
}))
}
}
Started::IfSameElse(yes, no) => {
self.rest = self.rest.trim_start();
let trimmed = c_onwards.len() - self.rest.len() - c_len;
self.pos += trimmed;
if self.rest.starts_with(c) {
let span = &c_onwards[..=c_len + trimmed];
self.rest = &self.rest[c_len..];
self.pos += c_len;
Some(Ok(Token {
origin: span.to_string(),
offset: c_at,
line: self.line,
kind: yes,
}))
} else {
Some(Ok(Token {
origin: c_str.to_string(),
offset: c_at,
line: self.line,
kind: no,
}))
}
}
Started::Ident => {
let first_non_ident = c_onwards[c_len..]
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(c_onwards.len() - c_len)
+ c_len;
let literal = &c_onwards[..first_non_ident];
let extra_bytes = literal.len() - c_len;
self.pos += extra_bytes;
self.rest = &self.rest[extra_bytes..];
let kind = match literal {
"false" => TokenKind::False,
"nil" => TokenKind::Nil,
"print" => TokenKind::Print,
"true" => TokenKind::True,
_ => self
.enums
.get(literal)
.map_or(TokenKind::Ident, |_value| TokenKind::Enum),
};
return Some(Ok(Token {
origin: literal.to_string(),
offset: c_at,
line: self.line,
kind,
}));
}
#[allow(clippy::redundant_guards)] Started::Number => {
if self.rest.starts_with('x') {
let number = &c_onwards[2..];
let first_non_hex_digit = number
.find(|c: char| !c.is_ascii_hexdigit())
.unwrap_or(c_onwards.len());
let end = min(first_non_hex_digit + 2, c_onwards.len());
let literal = &c_onwards[..end];
let extra_bytes = literal.len() - c_len;
self.pos += extra_bytes;
self.rest = &self.rest[extra_bytes..];
return Some(Ok(Token {
origin: literal.to_string(),
offset: c_at,
line: self.line,
kind: TokenKind::HexNumber,
}));
}
let first_non_digit = c_onwards
.find(|c| !matches!(c, '.' | '0'..='9'))
.unwrap_or(c_onwards.len());
let mut literal = &c_onwards[..first_non_digit];
let mut dotted = literal.splitn(3, '.');
match (dotted.next(), dotted.next(), dotted.next()) {
(Some(one), Some(two), Some(_)) => {
literal = &literal[..one.len() + 1 + two.len()];
}
(Some(one), Some(two), None) if two.is_empty() => {
literal = &literal[..one.len()];
}
_ => {
}
}
let extra_bytes = literal.len() - c_len;
self.pos += extra_bytes;
self.rest = &self.rest[extra_bytes..];
if literal.contains('.') {
return Some(Ok(Token {
origin: literal.to_string(),
offset: c_at,
line: self.line,
kind: TokenKind::FloatNumber,
}));
}
return Some(Ok(Token {
origin: literal.to_string(),
offset: c_at,
line: self.line,
kind: TokenKind::IntNumber,
}));
}
Started::String => {
if let Some(end) = self.rest.find('\'') {
let literal = &c_onwards[1..=(end)];
self.pos += end + 1;
self.rest = &self.rest[end + 1..];
Some(Ok(Token {
origin: literal.to_string(),
offset: c_at,
line: self.line,
kind: TokenKind::String,
}))
} else {
return Some(Err(CompilationError::UnterminatedString {
str: self.whole[c_at..].into(),
pos: self.line,
}));
}
}
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const fn is_normal<T: Sized + Send + Sync>() {}
#[test]
const fn normal_types() {
is_normal::<&Lexer>();
is_normal::<Lexer>();
}
}