xbasic 0.3.2

A library that allows adding a scripting language onto your project with ease. This lets your users write their own arbitrary logic.
Documentation
use crate::error_handler::ErrorHandler;
use crate::tokens::{Token, TokenType};
use phf::phf_map;
use std::str::FromStr;

static KEYWORDS: phf::Map<&'static str, TokenType> = phf_map! {
"and" => TokenType::And,
"else" => TokenType::Else,
"elseif" => TokenType::ElseIf,
"false" => TokenType::False,
"function" => TokenType::Function,
"fn" => TokenType::Function,
"if" => TokenType::If,
"or" => TokenType::Or,
"print" => TokenType::Print,
"input" => TokenType::Input,
"return" => TokenType::Return,
"true" => TokenType::True,
"while" => TokenType::While,
"not" => TokenType::Not,
"end" => TokenType::End,
"wend" => TokenType::Wend,
"for" => TokenType::For,
"to" => TokenType::To,
"next" => TokenType::Next,
"then" => TokenType::Then
};

pub struct Scanner<'a> {
	chars: Vec<char>,
	tokens: Vec<Token>,
	start: usize,
	current: usize,
	line: usize,
	error_handler: &'a mut ErrorHandler,
}

impl<'a> Scanner<'a> {
	pub fn new(source: &str, error_handler: &'a mut ErrorHandler) -> Self {
		let chars = source.chars().collect();
		Self {
			chars,
			tokens: Vec::new(),
			start: 0,
			current: 0,
			line: 1,
			error_handler,
		}
	}

	pub fn scan_tokens(&mut self) -> Result<Vec<Token>, ()> {
		while !self.is_at_end() {
			self.start = self.current;
			if let Err((line, msg)) = self.scan_token() {
				self.error_handler.error(line, &msg);
				return Err(());
			}
		}

		self.tokens
			.push(Token::new(TokenType::Eof, "".to_string(), self.line));

		Ok(self.tokens.clone())
	}

	pub fn scan_token(&mut self) -> Result<(), (usize, String)> {
		let c = self.advance();
		let token = match c {
			'(' => TokenType::LeftParen,
			')' => TokenType::RightParen,
			'{' => TokenType::LeftBrace,
			'}' => TokenType::RightBrace,
			',' => TokenType::Comma,
			'.' => TokenType::Dot,
			'-' => {
				if self.match_next('-') {
					TokenType::MinusMinus
				} else if self.match_next('=') {
					TokenType::MinusEqual
				} else {
					TokenType::Minus
				}
			}
			'<' => {
				if self.match_next('=') {
					TokenType::LessEqual
				} else {
					TokenType::Less
				}
			}
			'>' => {
				if self.match_next('=') {
					TokenType::GreaterEqual
				} else {
					TokenType::Greater
				}
			}
			'+' => {
				if self.match_next('+') {
					TokenType::PlusPlus
				} else if self.match_next('=') {
					TokenType::PlusEqual
				} else {
					TokenType::Plus
				}
			}
			'*' => {
				if self.match_next('=') {
					TokenType::StarEqual
				} else {
					TokenType::Star
				}
			}
			'/' => {
				if self.match_next('/') {
					while self.peek() != '\n' && !self.is_at_end() {
						self.advance();
					}
					TokenType::NoToken
				} else if self.match_next('=') {
					TokenType::SlashEqual
				} else {
					TokenType::Slash
				}
			}
			'^' => {
				if self.match_next('=') {
					TokenType::CaretEqual
				} else {
					TokenType::Caret
				}
			}
			'%' => {
				if self.match_next('=') {
					TokenType::PercentEqual
				} else {
					TokenType::Percent
				}
			}
			'=' => TokenType::Equal,
			' ' | '\r' | '\t' => TokenType::NoToken,
			'\n' => {
				self.line += 1;
				let last_newline = match self.tokens.last() {
					Some(x) => x.token_type == TokenType::Newline,
					None => false,
				};

				if last_newline {
					TokenType::NoToken
				} else {
					TokenType::Newline
				}
			}
			'"' => {
				if let Err(x) = self.string() {
					return Err(x);
				}
				TokenType::NoToken
			}
			'0'..='9' => {
				self.number();
				TokenType::NoToken
			}
			_ => {
				if is_alpha(c) {
					self.identifier();
					TokenType::NoToken
				} else {
					return Err((self.line, "Unexpected character.".to_string()));
				}
			}
		};

		self.add_token(token);

		Ok(())
	}

	fn is_at_end(&self) -> bool {
		self.current >= self.chars.len()
	}

	fn add_token(&mut self, token_type: TokenType) {
		if token_type == TokenType::NoToken {
			return;
		}
		let text: String = self.chars[self.start..self.current].iter().collect();
		self.tokens.push(Token::new(token_type, text, self.line));
	}

	fn advance(&mut self) -> char {
		self.current += 1;
		self.chars[self.current - 1]
	}

	fn previous(&self) -> char {
		if self.current == 0 {
			return '\0';
		}

		self.chars[self.current - 1]
	}

	fn peek(&self) -> char {
		if self.is_at_end() {
			return '\0';
		}
		self.chars[self.current]
	}

	fn peek_next(&self) -> char {
		if self.current + 1 >= self.chars.len() {
			return '\0';
		}

		self.chars[self.current + 1]
	}

	fn match_next(&mut self, expected: char) -> bool {
		if self.is_at_end() {
			return false;
		}
		if self.chars[self.current] != expected {
			return false;
		}

		self.current += 1;
		true
	}

	fn string(&mut self) -> Result<(), (usize, String)> {
		while (self.peek() != '"' || self.previous() == '\\') && !self.is_at_end() {
			if self.peek() == '\n' {
				self.line += 1;
			}
			self.advance();
		}

		if self.is_at_end() {
			return Err((self.line, "Unterminated string.".to_string()));
		}

		self.advance(); // Closing "

		let mut value: String = String::new();
		let mut last = '\0';
		for &char in self.chars[(self.start + 1)..(self.current - 1)].iter() {
			let mut null_last = false;
			if char == 'n' && last == '\\' {
				value.push('\n');
				null_last = true;
			} else if char == '"' && last == '\\' {
				value.push('"');
				null_last = true;
			} else if char == '\\' && last == '\\' {
				value.push('\\');
				null_last = true;
			} else {
				if last == '\\' {
					value.push('\\');
				}
				if char != '\\' {
					value.push(char);
				}
			}

			if null_last {
				last = '\0';
			} else {
				last = char;
			}
		}
		self.add_token(TokenType::String(value));

		Ok(())
	}

	fn number(&mut self) {
		while is_digit(self.peek()) {
			self.advance();
		}

		if self.peek() == '.' && is_digit(self.peek_next()) {
			self.advance(); // Consume "."

			while is_digit(self.peek()) {
				self.advance();
			} // Keep consuming digits
		}

		let next_char = self.peek();

		let s: String = self.chars[self.start..self.current].iter().collect();

		// Consume variable type, if available
		if next_char == 'i' || next_char == 'f' {
			self.advance();
		}

		// TODO don't unwrap here
		let t = match next_char {
			'i' => TokenType::Integer(i64::from_str(&s).unwrap_or_else(|_| {
				self.error_handler
					.error(self.line, &format!("Cannot parse {} as integer", s));
				0
			})),
			_ => TokenType::Decimal(f64::from_str(&s).unwrap_or_else(|_| {
				self.error_handler
					.error(self.line, &format!("Cannot parse {} as decimal", s));
				0.0
			})),
		};

		self.add_token(t)
	}

	fn identifier(&mut self) {
		while is_alphanumeric(self.peek()) {
			self.advance();
		}

		let text: String = self.chars[self.start..self.current].iter().collect();
		let token: TokenType = match KEYWORDS.get(text.as_str()) {
			Some(x) => x.clone(),
			None => TokenType::Identifier,
		};

		self.add_token(token);
	}
}

fn is_digit(c: char) -> bool {
	('0'..='9').contains(&c)
}

fn is_alpha(c: char) -> bool {
	('a'..='z').contains(&c) || ('A'..='Z').contains(&c) || c == '_'
}

fn is_alphanumeric(c: char) -> bool {
	is_digit(c) || is_alpha(c)
}