Skip to main content

rl_lang/lexer/
tokenizer.rs

1//! [`Tokenizer`] struct and the main character-by-character scan loop.
2//!
3//! Holds the source text, the current byte cursor, and the accumulated token
4//! list. The scan loop in here dispatches each character to the appropriate
5//! sub-scanner in `types/` or handles single/double-character operators inline.
6use crate::lexer::tokentypes::{Token, TokenType};
7use crate::utils::errors::Error;
8use crate::utils::source::SourceFile;
9use crate::utils::span::Span;
10
11/// Converts raw source text into a flat list of [`Token`]s.
12///
13/// Operates character by character, grouping lexemes into tokens.
14/// The main entry point is [`Tokenizer::lex`].
15pub struct Tokenizer {
16    /// The source file (text + name) used for error reporting.
17    pub source_file: SourceFile,
18    /// The source text as a sequence of characters.
19    pub source: Vec<char>,
20    /// The accumulated token list built during lexing.
21    pub tokens: Vec<super::tokentypes::Token>,
22    /// Index of the current character being examined.
23    pub current: usize,
24    /// Index of the first character of the current token.
25    pub start: usize,
26    /// Current line number, incremented on every `\n`.
27    pub line: usize,
28}
29
30impl Tokenizer {
31    /// The main entry point: lexes a [`SourceFile`] into a [`Vec<Token>`].
32    ///
33    /// Drives [`Tokenizer::scan_tokens`] in a loop until the source is exhausted,
34    /// then appends a [`TokenType::Eof`] so the parser always has a clean terminator.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`Error`] if the source contains an unrecognized character,
39    /// unterminated string, or invalid literal.
40    ///
41    /// # Examples
42    ///
43    /// ```rust
44    /// use rl_lang::{
45    ///     lexer::{
46    ///         tokenizer::Tokenizer,
47    ///         tokentypes::TokenType,
48    ///     },
49    ///     utils::source::SourceFile,
50    /// };
51    ///
52    /// let tokens = match Tokenizer::lex(SourceFile::new("source", "1 == 1".to_string())) {
53    ///     Ok(tokens) => tokens,
54    ///     Err(error) => {
55    ///         error.report_to_stderr();
56    ///         std::process::exit(1);
57    ///     },
58    /// };
59    ///
60    /// assert_eq!(tokens[0].token, TokenType::NumberLiteral(1));
61    /// assert_eq!(tokens[1].token, TokenType::Compare);
62    /// assert_eq!(tokens[2].token, TokenType::NumberLiteral(1));
63    /// assert_eq!(tokens[3].token, TokenType::Eof);
64    /// ```
65    pub fn lex(source_file: SourceFile) -> Result<Vec<Token>, Error> {
66        let chars: Vec<char> = source_file.text.chars().collect();
67        let eof_char_index = chars.len();
68
69        let mut lexer = Tokenizer {
70            source_file,
71            source: chars,
72            tokens: Vec::new(),
73            current: 0,
74            start: 0,
75            line: 1,
76        };
77
78        while !lexer.is_at_end() {
79            lexer.start = lexer.current;
80            lexer.scan_tokens()?;
81        }
82
83        lexer.tokens.push(Token::new(
84            TokenType::Eof,
85            String::new(),
86            lexer.line,
87            Span::new(eof_char_index, eof_char_index),
88        ));
89
90        #[cfg(feature = "debug")]
91        log::debug!("Recognized {} token(s)", lexer.tokens.len());
92        Ok(lexer.tokens)
93    }
94
95    /// Returns a [`Span`] covering the current token in character indices.
96    ///
97    /// Ariadne's `Source::from(&str)` indexes by character, so spans must be
98    /// char-indexed - passing byte offsets misaligns reports for multi-byte characters.
99    pub fn current_span(&self) -> Span {
100        Span::new(self.start, self.current)
101    }
102
103    /// Builds a [`Reason::Lexer`] error anchored at `span`.
104    ///
105    /// Attaches the source file so Ariadne can render the relevant source line
106    /// alongside the error message.
107    pub fn err(&self, message: impl Into<String>, span: Span) -> Error {
108        Error::at(crate::utils::errors::Reason::Lexer, message, span)
109            .with_source_file(&self.source_file)
110    }
111}