2025-10-07 14:28:24 +02:00
|
|
|
use crate::common::base_value::{BaseValue, Number};
|
|
|
|
|
use crate::common::lox_result::{lexical_error, LoxError, LoxResult};
|
2025-10-03 19:07:12 +02:00
|
|
|
use crate::frontend::source_registry::{SourceId, SourcePosition, SourceSlice};
|
2025-10-07 14:28:24 +02:00
|
|
|
use crate::frontend::tokens::{Token, TokenType};
|
2025-10-03 19:07:12 +02:00
|
|
|
|
|
|
|
|
pub struct Lexer {
|
|
|
|
|
input: String,
|
2026-06-30 14:05:46 +02:00
|
|
|
// Byte offsets into `input` (not char counts) so slicing is UTF-8 correct.
|
|
|
|
|
start: usize,
|
|
|
|
|
current: usize,
|
2025-10-03 19:07:12 +02:00
|
|
|
start_pos: SourcePosition,
|
|
|
|
|
end_pos: SourcePosition,
|
|
|
|
|
source_id: SourceId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_keyword_token(word: &str) -> Option<TokenType> {
|
|
|
|
|
match word {
|
|
|
|
|
"and" => Some(TokenType::And),
|
2026-07-06 10:43:17 +02:00
|
|
|
"struct" => Some(TokenType::Struct),
|
2025-10-03 19:07:12 +02:00
|
|
|
"do" => Some(TokenType::StartBlock),
|
|
|
|
|
"end" => Some(TokenType::EndBlock),
|
|
|
|
|
"false" => Some(TokenType::False),
|
|
|
|
|
"for" => Some(TokenType::For),
|
|
|
|
|
"fun" => Some(TokenType::Fun),
|
|
|
|
|
"if" => Some(TokenType::If),
|
2025-10-04 19:02:33 +02:00
|
|
|
"then" => Some(TokenType::Then),
|
2025-10-03 19:07:12 +02:00
|
|
|
"elif" => Some(TokenType::Elif),
|
|
|
|
|
"else" => Some(TokenType::Else),
|
|
|
|
|
"or" => Some(TokenType::Or),
|
2025-10-04 21:05:00 +02:00
|
|
|
"in" => Some(TokenType::In),
|
2025-10-03 19:07:12 +02:00
|
|
|
"print" => Some(TokenType::Print),
|
|
|
|
|
"return" => Some(TokenType::Return),
|
2026-02-11 16:35:05 +01:00
|
|
|
"break" => Some(TokenType::Break),
|
2025-10-03 19:07:12 +02:00
|
|
|
"super" => Some(TokenType::Super),
|
|
|
|
|
"this" => Some(TokenType::This),
|
|
|
|
|
"true" => Some(TokenType::True),
|
|
|
|
|
"var" => Some(TokenType::Var),
|
|
|
|
|
"val" => Some(TokenType::Val),
|
|
|
|
|
"while" => Some(TokenType::While),
|
|
|
|
|
"True" => Some(TokenType::True),
|
|
|
|
|
"False" => Some(TokenType::False),
|
|
|
|
|
"Nil" => Some(TokenType::Nil),
|
2025-10-06 18:52:32 +02:00
|
|
|
"fn" => Some(TokenType::Fn),
|
2025-10-03 19:07:12 +02:00
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Lexer {
|
|
|
|
|
pub fn new(input: String, source_id: SourceId) -> Lexer {
|
|
|
|
|
Lexer {
|
|
|
|
|
input,
|
2026-06-30 14:05:46 +02:00
|
|
|
start: 0,
|
|
|
|
|
current: 0,
|
2025-10-03 19:07:12 +02:00
|
|
|
start_pos: SourcePosition::default(),
|
|
|
|
|
end_pos: SourcePosition::default(),
|
|
|
|
|
source_id,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn scans_tokens(&mut self) -> LoxResult<Vec<Token>> {
|
|
|
|
|
let mut tokens = Vec::new();
|
|
|
|
|
while !self.is_at_end() {
|
2026-06-30 14:05:46 +02:00
|
|
|
self.start = self.current;
|
2025-10-03 19:07:12 +02:00
|
|
|
self.start_pos = self.end_pos.clone();
|
|
|
|
|
match self.scan_token() {
|
|
|
|
|
Ok(Some(token)) => tokens.push(token),
|
|
|
|
|
Ok(None) => {}
|
|
|
|
|
Err(err) => return Err(err),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tokens.push(self.make_token(TokenType::Eof));
|
|
|
|
|
Ok(tokens)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_at_end(&self) -> bool {
|
2026-06-30 14:05:46 +02:00
|
|
|
self.current >= self.input.len()
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn advance(&mut self) -> char {
|
2026-06-30 14:05:46 +02:00
|
|
|
let c = self.input[self.current..].chars().next().unwrap();
|
|
|
|
|
self.current += c.len_utf8();
|
|
|
|
|
if c == '\n' {
|
|
|
|
|
self.end_pos.line += 1;
|
|
|
|
|
self.end_pos.column = 0;
|
|
|
|
|
} else {
|
|
|
|
|
self.end_pos.column += 1;
|
|
|
|
|
}
|
|
|
|
|
c
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn peek(&self) -> char {
|
2026-06-30 14:05:46 +02:00
|
|
|
self.input[self.current..].chars().next().unwrap_or('\0')
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn peek_next(&self) -> char {
|
2026-06-30 14:05:46 +02:00
|
|
|
self.input[self.current..].chars().nth(1).unwrap_or('\0')
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn make_token(&self, token_type: TokenType) -> Token {
|
2026-06-30 14:05:46 +02:00
|
|
|
let text = self.input[self.start..self.current].to_string();
|
2025-10-03 19:07:12 +02:00
|
|
|
Token::new(
|
|
|
|
|
token_type,
|
|
|
|
|
text,
|
|
|
|
|
SourceSlice::from_positions(
|
|
|
|
|
self.source_id.clone(),
|
|
|
|
|
self.start_pos.clone(),
|
|
|
|
|
self.end_pos.clone(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-10-07 14:28:24 +02:00
|
|
|
fn make_token_with_literal(&self, token_type: TokenType, literal: BaseValue) -> Token {
|
2026-06-30 14:05:46 +02:00
|
|
|
let text = self.input[self.start..self.current].to_string();
|
2025-10-03 19:07:12 +02:00
|
|
|
Token::new_complete(
|
|
|
|
|
token_type,
|
|
|
|
|
text,
|
|
|
|
|
Some(literal),
|
|
|
|
|
SourceSlice::from_positions(
|
|
|
|
|
self.source_id.clone(),
|
|
|
|
|
self.start_pos.clone(),
|
|
|
|
|
self.end_pos.clone(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn scan_token(&mut self) -> LoxResult<Option<Token>> {
|
|
|
|
|
let c = self.advance();
|
|
|
|
|
match (c, self.peek()) {
|
|
|
|
|
('(', _) => Ok(Some(self.make_token(TokenType::LeftParen))),
|
|
|
|
|
(')', _) => Ok(Some(self.make_token(TokenType::RightParen))),
|
|
|
|
|
('{', _) => Ok(Some(self.make_token(TokenType::LeftBrace))),
|
|
|
|
|
('}', _) => Ok(Some(self.make_token(TokenType::RightBrace))),
|
|
|
|
|
('[', _) => Ok(Some(self.make_token(TokenType::LeftBracket))),
|
|
|
|
|
(']', _) => Ok(Some(self.make_token(TokenType::RightBracket))),
|
|
|
|
|
(',', _) => Ok(Some(self.make_token(TokenType::Comma))),
|
|
|
|
|
('.', _) => Ok(Some(self.make_token(TokenType::Dot))),
|
|
|
|
|
('-', _) => Ok(Some(self.make_token(TokenType::Minus))),
|
|
|
|
|
('+', _) => Ok(Some(self.make_token(TokenType::Plus))),
|
|
|
|
|
(';', _) => Ok(Some(self.make_token(TokenType::Semicolon))),
|
2025-10-06 18:52:32 +02:00
|
|
|
(':', _) => Ok(Some(self.make_token(TokenType::Colon))),
|
2025-10-03 19:07:12 +02:00
|
|
|
('*', _) => Ok(Some(self.make_token(TokenType::Star))),
|
2025-10-04 21:05:00 +02:00
|
|
|
('%', _) => Ok(Some(self.make_token(TokenType::Percent))),
|
2025-10-03 19:07:12 +02:00
|
|
|
('!', '=') => {
|
|
|
|
|
self.advance(); // consuma il '='
|
|
|
|
|
Ok(Some(self.make_token(TokenType::BangEqual)))
|
|
|
|
|
}
|
|
|
|
|
('!', _) => Ok(Some(self.make_token(TokenType::Bang))),
|
|
|
|
|
('=', '=') => {
|
|
|
|
|
self.advance(); // consuma il '='
|
|
|
|
|
Ok(Some(self.make_token(TokenType::EqualEqual)))
|
|
|
|
|
}
|
|
|
|
|
('=', _) => Ok(Some(self.make_token(TokenType::Equal))),
|
|
|
|
|
('<', '=') => {
|
|
|
|
|
self.advance(); // consuma il '='
|
|
|
|
|
Ok(Some(self.make_token(TokenType::LessEqual)))
|
|
|
|
|
}
|
|
|
|
|
('<', _) => Ok(Some(self.make_token(TokenType::Less))),
|
|
|
|
|
('>', '=') => {
|
|
|
|
|
self.advance(); // consuma il '='
|
|
|
|
|
Ok(Some(self.make_token(TokenType::GreaterEqual)))
|
|
|
|
|
}
|
|
|
|
|
('>', _) => Ok(Some(self.make_token(TokenType::Greater))),
|
|
|
|
|
('/', '/') => {
|
|
|
|
|
// Commento single-line
|
|
|
|
|
while self.peek() != '\n' && !self.is_at_end() {
|
|
|
|
|
self.advance();
|
|
|
|
|
}
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
('/', '*') => {
|
|
|
|
|
// Commento multi-line
|
|
|
|
|
while (self.peek() != '*' || self.peek_next() != '/') && !self.is_at_end() {
|
|
|
|
|
self.advance();
|
|
|
|
|
}
|
|
|
|
|
if self.is_at_end() {
|
2025-10-07 14:28:24 +02:00
|
|
|
return lexical_error(
|
|
|
|
|
SourceSlice::from_positions(
|
2025-10-03 19:07:12 +02:00
|
|
|
self.source_id.clone(),
|
|
|
|
|
self.start_pos.clone(),
|
|
|
|
|
self.end_pos.clone(),
|
|
|
|
|
),
|
2025-10-07 14:28:24 +02:00
|
|
|
"Unterminated comment".to_string(),
|
|
|
|
|
);
|
2025-10-03 19:07:12 +02:00
|
|
|
} else {
|
|
|
|
|
self.advance(); // consuma '*'
|
|
|
|
|
self.advance(); // consuma '/'
|
|
|
|
|
}
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
('/', _) => Ok(Some(self.make_token(TokenType::Slash))),
|
|
|
|
|
(' ', _) | ('\r', _) | ('\t', _) => Ok(None),
|
2026-06-30 14:05:46 +02:00
|
|
|
('\n', _) => Ok(None),
|
2025-10-03 19:07:12 +02:00
|
|
|
('"', _) => self.string(),
|
|
|
|
|
(c, _) if c.is_digit(10) => self.number(),
|
|
|
|
|
(c, _) if c.is_alphanumeric() || c == '_' => self.identifier(),
|
|
|
|
|
_ => Err(LoxError::LexicalError {
|
|
|
|
|
source_slice: SourceSlice::from_positions(
|
|
|
|
|
self.source_id.clone(),
|
|
|
|
|
self.start_pos.clone(),
|
|
|
|
|
self.end_pos.clone(),
|
|
|
|
|
),
|
|
|
|
|
message: format!("Unexpected character: {}", c),
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn string(&mut self) -> LoxResult<Option<Token>> {
|
|
|
|
|
while self.peek() != '"' && !self.is_at_end() {
|
|
|
|
|
self.advance();
|
|
|
|
|
}
|
|
|
|
|
if self.is_at_end() {
|
|
|
|
|
return Err(LoxError::LexicalError {
|
|
|
|
|
source_slice: SourceSlice::from_positions(
|
|
|
|
|
self.source_id.clone(),
|
|
|
|
|
self.start_pos.clone(),
|
|
|
|
|
self.end_pos.clone(),
|
|
|
|
|
),
|
|
|
|
|
message: "Unterminated string".to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
self.advance();
|
|
|
|
|
Ok(Some(self.make_token_with_literal(
|
|
|
|
|
TokenType::String,
|
2026-06-30 14:05:46 +02:00
|
|
|
BaseValue::String(self.input[self.start..self.current].to_string()),
|
2025-10-03 19:07:12 +02:00
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn number(&mut self) -> LoxResult<Option<Token>> {
|
2025-10-07 14:28:24 +02:00
|
|
|
// Leggi la parte intera
|
2025-10-03 19:07:12 +02:00
|
|
|
while self.peek().is_digit(10) {
|
|
|
|
|
self.advance();
|
|
|
|
|
}
|
2025-10-07 14:28:24 +02:00
|
|
|
|
|
|
|
|
// Controlla se c'è una parte decimale
|
|
|
|
|
let has_decimal = if self.peek() == '.' && self.peek_next().is_digit(10) {
|
|
|
|
|
self.advance(); // consuma il '.'
|
2025-10-03 19:07:12 +02:00
|
|
|
while self.peek().is_digit(10) {
|
|
|
|
|
self.advance();
|
|
|
|
|
}
|
2025-10-07 14:28:24 +02:00
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Controlla se c'è un suffisso (f o u)
|
|
|
|
|
let suffix = if self.peek() == 'f' || self.peek() == 'u' {
|
|
|
|
|
let s = self.peek();
|
|
|
|
|
self.advance();
|
|
|
|
|
Some(s)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-30 14:05:46 +02:00
|
|
|
let num_str = &self.input[self.start..self.current];
|
2025-10-07 14:28:24 +02:00
|
|
|
let num_str_without_suffix = if suffix.is_some() {
|
|
|
|
|
&num_str[..num_str.len() - 1]
|
|
|
|
|
} else {
|
|
|
|
|
num_str
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Determina il tipo di numero basandosi sul contenuto e suffisso
|
|
|
|
|
let number_value = match suffix {
|
|
|
|
|
Some('f') => {
|
|
|
|
|
// Float esplicito con suffisso 'f'
|
|
|
|
|
Number::F64(num_str_without_suffix.parse().unwrap())
|
|
|
|
|
}
|
|
|
|
|
Some('u') => {
|
|
|
|
|
// Unsigned esplicito con suffisso 'u'
|
|
|
|
|
Number::U128(num_str_without_suffix.parse().unwrap())
|
|
|
|
|
}
|
|
|
|
|
_ if has_decimal => {
|
|
|
|
|
// Float implicito (contiene un punto decimale)
|
|
|
|
|
Number::F64(num_str.parse().unwrap())
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
// Intero con segno di default
|
|
|
|
|
Number::I32(num_str.parse().unwrap())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Some(self.make_token_with_literal(
|
|
|
|
|
TokenType::Number,
|
|
|
|
|
BaseValue::Number(number_value),
|
|
|
|
|
)))
|
2025-10-03 19:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn identifier(&mut self) -> LoxResult<Option<Token>> {
|
2025-10-06 18:52:32 +02:00
|
|
|
while self.peek().is_alphanumeric() || self.peek() == '_' {
|
2025-10-03 19:07:12 +02:00
|
|
|
self.advance();
|
|
|
|
|
}
|
2026-06-30 14:05:46 +02:00
|
|
|
let text = self.input[self.start..self.current].to_string();
|
2025-10-03 19:07:12 +02:00
|
|
|
match get_keyword_token(&text) {
|
|
|
|
|
Some(TokenType::True) => Ok(Some(
|
2025-10-07 14:28:24 +02:00
|
|
|
self.make_token_with_literal(TokenType::True, BaseValue::Boolean(true)),
|
2025-10-03 19:07:12 +02:00
|
|
|
)),
|
|
|
|
|
Some(TokenType::False) => Ok(Some(
|
2025-10-07 14:28:24 +02:00
|
|
|
self.make_token_with_literal(TokenType::False, BaseValue::Boolean(false)),
|
2025-10-03 19:07:12 +02:00
|
|
|
)),
|
|
|
|
|
Some(TokenType::Nil) => Ok(Some(
|
2025-10-07 14:28:24 +02:00
|
|
|
self.make_token_with_literal(TokenType::Nil, BaseValue::Nil),
|
2025-10-03 19:07:12 +02:00
|
|
|
)),
|
|
|
|
|
Some(token_type) => Ok(Some(self.make_token(token_type))),
|
|
|
|
|
None => Ok(Some(self.make_token_with_literal(
|
|
|
|
|
TokenType::Identifier,
|
2025-10-07 14:28:24 +02:00
|
|
|
BaseValue::String(text.clone()),
|
2025-10-03 19:07:12 +02:00
|
|
|
))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-29 20:47:59 +02:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::f64;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
/// Lex `input` and return the resulting tokens, panicking on lexical errors.
|
|
|
|
|
fn lex(input: &str) -> Vec<Token> {
|
|
|
|
|
Lexer::new(input.to_string(), 0)
|
|
|
|
|
.scans_tokens()
|
|
|
|
|
.expect("expected input to lex without errors")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lex `input` and collect just the token types (including the trailing EOF).
|
|
|
|
|
fn token_types(input: &str) -> Vec<TokenType> {
|
|
|
|
|
lex(input).into_iter().map(|t| t.token_type).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_single_character_tokens() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
token_types("(){}[],.-+;:*%"),
|
|
|
|
|
vec![
|
|
|
|
|
TokenType::LeftParen,
|
|
|
|
|
TokenType::RightParen,
|
|
|
|
|
TokenType::LeftBrace,
|
|
|
|
|
TokenType::RightBrace,
|
|
|
|
|
TokenType::LeftBracket,
|
|
|
|
|
TokenType::RightBracket,
|
|
|
|
|
TokenType::Comma,
|
|
|
|
|
TokenType::Dot,
|
|
|
|
|
TokenType::Minus,
|
|
|
|
|
TokenType::Plus,
|
|
|
|
|
TokenType::Semicolon,
|
|
|
|
|
TokenType::Colon,
|
|
|
|
|
TokenType::Star,
|
|
|
|
|
TokenType::Percent,
|
|
|
|
|
TokenType::Eof,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_one_and_two_char_operators() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
token_types("! != = == < <= > >= /"),
|
|
|
|
|
vec![
|
|
|
|
|
TokenType::Bang,
|
|
|
|
|
TokenType::BangEqual,
|
|
|
|
|
TokenType::Equal,
|
|
|
|
|
TokenType::EqualEqual,
|
|
|
|
|
TokenType::Less,
|
|
|
|
|
TokenType::LessEqual,
|
|
|
|
|
TokenType::Greater,
|
|
|
|
|
TokenType::GreaterEqual,
|
|
|
|
|
TokenType::Slash,
|
|
|
|
|
TokenType::Eof,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn keyword_lookup_matches_known_words() {
|
|
|
|
|
assert_eq!(get_keyword_token("and"), Some(TokenType::And));
|
|
|
|
|
assert_eq!(get_keyword_token("if"), Some(TokenType::If));
|
|
|
|
|
assert_eq!(get_keyword_token("then"), Some(TokenType::Then));
|
|
|
|
|
assert_eq!(get_keyword_token("while"), Some(TokenType::While));
|
|
|
|
|
assert_eq!(get_keyword_token("do"), Some(TokenType::StartBlock));
|
|
|
|
|
assert_eq!(get_keyword_token("end"), Some(TokenType::EndBlock));
|
|
|
|
|
assert_eq!(get_keyword_token("not_a_keyword"), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_keywords_in_a_stream() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
token_types("if then else while print return"),
|
|
|
|
|
vec![
|
|
|
|
|
TokenType::If,
|
|
|
|
|
TokenType::Then,
|
|
|
|
|
TokenType::Else,
|
|
|
|
|
TokenType::While,
|
|
|
|
|
TokenType::Print,
|
|
|
|
|
TokenType::Return,
|
|
|
|
|
TokenType::Eof,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_integer_number() {
|
|
|
|
|
let tokens = lex("42");
|
|
|
|
|
assert_eq!(tokens[0].token_type, TokenType::Number);
|
|
|
|
|
assert_eq!(tokens[0].literal, Some(BaseValue::Number(Number::I32(42))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_float_number() {
|
|
|
|
|
let tokens = lex("3.141592653589793");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
tokens[0].literal,
|
|
|
|
|
Some(BaseValue::Number(Number::F64(f64::consts::PI)))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_number_with_float_suffix() {
|
|
|
|
|
let tokens = lex("5f");
|
|
|
|
|
assert_eq!(tokens[0].literal, Some(BaseValue::Number(Number::F64(5.0))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_number_with_unsigned_suffix() {
|
|
|
|
|
let tokens = lex("7u");
|
|
|
|
|
assert_eq!(tokens[0].literal, Some(BaseValue::Number(Number::U128(7))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_string_literal_including_quotes() {
|
|
|
|
|
// The lexer slices from the opening quote through the closing quote,
|
|
|
|
|
// so the literal currently retains the surrounding quotes.
|
|
|
|
|
let tokens = lex("\"hello\"");
|
|
|
|
|
assert_eq!(tokens[0].token_type, TokenType::String);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
tokens[0].literal,
|
|
|
|
|
Some(BaseValue::String("\"hello\"".to_string()))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_identifier() {
|
|
|
|
|
let tokens = lex("foo_bar");
|
|
|
|
|
assert_eq!(tokens[0].token_type, TokenType::Identifier);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
tokens[0].literal,
|
|
|
|
|
Some(BaseValue::String("foo_bar".to_string()))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn scans_boolean_and_nil_literals() {
|
|
|
|
|
let tokens = lex("true false Nil");
|
|
|
|
|
assert_eq!(tokens[0].literal, Some(BaseValue::Boolean(true)));
|
|
|
|
|
assert_eq!(tokens[1].literal, Some(BaseValue::Boolean(false)));
|
|
|
|
|
assert_eq!(tokens[2].literal, Some(BaseValue::Nil));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ignores_line_comments() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
token_types("1 // a comment\n2"),
|
|
|
|
|
vec![TokenType::Number, TokenType::Number, TokenType::Eof]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ignores_block_comments() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
token_types("1 /* multi\nline */ 2"),
|
|
|
|
|
vec![TokenType::Number, TokenType::Number, TokenType::Eof]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn always_appends_eof_even_for_empty_input() {
|
|
|
|
|
let tokens = lex("");
|
|
|
|
|
assert_eq!(tokens.len(), 1);
|
|
|
|
|
assert_eq!(tokens[0].token_type, TokenType::Eof);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unterminated_string_is_a_lexical_error() {
|
|
|
|
|
let result = Lexer::new("\"oops".to_string(), 0).scans_tokens();
|
|
|
|
|
assert!(matches!(result, Err(LoxError::LexicalError { .. })));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unterminated_block_comment_is_a_lexical_error() {
|
|
|
|
|
let result = Lexer::new("/* never ends".to_string(), 0).scans_tokens();
|
|
|
|
|
assert!(matches!(result, Err(LoxError::LexicalError { .. })));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unexpected_character_is_a_lexical_error() {
|
|
|
|
|
let result = Lexer::new("@".to_string(), 0).scans_tokens();
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
}
|
2026-06-30 14:05:46 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handles_multibyte_identifier() {
|
|
|
|
|
// `é` is two UTF-8 bytes; the old char-counted slicing would panic or
|
|
|
|
|
// slice mid-codepoint here. Byte offsets make this correct.
|
|
|
|
|
let tokens = lex("café");
|
|
|
|
|
assert_eq!(tokens[0].token_type, TokenType::Identifier);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
tokens[0].literal,
|
|
|
|
|
Some(BaseValue::String("café".to_string()))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn tracks_line_and_column_across_newlines() {
|
|
|
|
|
// "1\n22": the second token sits at the start of line 1.
|
|
|
|
|
let tokens = lex("1\n22");
|
|
|
|
|
assert_eq!(tokens[1].lexeme, "22");
|
|
|
|
|
assert_eq!(tokens[1].source_slice.start_position.line, 1);
|
|
|
|
|
assert_eq!(tokens[1].source_slice.start_position.column, 0);
|
|
|
|
|
}
|
2026-07-06 10:43:17 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handle_struct() {
|
|
|
|
|
let tokens = lex("Cosa :: struct {field1: Number, field2: String}");
|
|
|
|
|
let tokens_tyepe: Vec<TokenType> = tokens.iter().map(|x| x.token_type.clone()).collect();
|
|
|
|
|
let tokent_for_check = vec![
|
|
|
|
|
TokenType::Identifier,
|
|
|
|
|
TokenType::Colon,
|
|
|
|
|
TokenType::Colon,
|
|
|
|
|
TokenType::Struct,
|
|
|
|
|
TokenType::LeftBrace,
|
|
|
|
|
TokenType::Identifier,
|
|
|
|
|
TokenType::Colon,
|
|
|
|
|
TokenType::Identifier,
|
|
|
|
|
TokenType::Comma,
|
|
|
|
|
TokenType::Identifier,
|
|
|
|
|
TokenType::Colon,
|
|
|
|
|
TokenType::Identifier,
|
|
|
|
|
TokenType::RightBrace,
|
|
|
|
|
TokenType::Eof,
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(tokens_tyepe, tokent_for_check);
|
|
|
|
|
assert_eq!(tokens[0].lexeme, "Cosa");
|
|
|
|
|
assert_eq!(tokens[5].lexeme, "field1");
|
|
|
|
|
assert_eq!(tokens[7].lexeme, "Number");
|
|
|
|
|
assert_eq!(tokens[9].lexeme, "field2");
|
|
|
|
|
assert_eq!(tokens[11].lexeme, "String");
|
|
|
|
|
}
|
2026-06-29 20:47:59 +02:00
|
|
|
}
|