Skip to main content

boa_parser/lexer/
mod.rs

1//! Boa's lexical analyzer(Lexer) for ECMAScript source code.
2//!
3//! The Lexer splits its input source code into a sequence of input elements called tokens,
4//! represented by the [Token] structure. It also removes
5//! whitespace and comments and attaches them to the next token.
6//!
7//! This is tightly coupled with the parser due to the javascript goal-symbol requirements
8//! as documented by the spec.
9//!
10//! More information:
11//!  - [ECMAScript reference][spec]
12//!
13//! [spec]: https://tc39.es/ecma262/#sec-ecmascript-language-lexical-grammar
14
15pub mod error;
16pub mod regex;
17pub mod token;
18
19mod comment;
20mod cursor;
21mod identifier;
22mod number;
23mod operator;
24mod private_identifier;
25mod spread;
26mod string;
27mod template;
28
29#[cfg(test)]
30mod tests;
31
32use self::{
33    comment::{HashbangComment, MultiLineComment, SingleLineComment},
34    cursor::Cursor,
35    identifier::Identifier,
36    number::NumberLiteral,
37    operator::Operator,
38    private_identifier::PrivateIdentifier,
39    regex::RegexLiteral,
40    spread::SpreadLiteral,
41    string::StringLiteral,
42    template::TemplateLiteral,
43};
44use crate::source::{ReadChar, UTF8Input};
45use boa_ast::{PositionGroup, Punctuator};
46use boa_interner::Interner;
47
48pub use self::{
49    error::Error,
50    token::{Token, TokenKind},
51};
52
53trait Tokenizer<R> {
54    /// Lexes the next token.
55    fn lex(
56        &mut self,
57        cursor: &mut Cursor<R>,
58        start_pos: PositionGroup,
59        interner: &mut Interner,
60    ) -> Result<Token, Error>
61    where
62        R: ReadChar;
63}
64
65/// Lexer or tokenizer for the Boa JavaScript Engine.
66#[derive(Debug)]
67pub struct Lexer<R> {
68    cursor: Cursor<R>,
69    goal_symbol: InputElement,
70}
71
72impl<R> Lexer<R> {
73    /// Sets the goal symbol for the lexer.
74    pub(crate) fn set_goal(&mut self, elm: InputElement) {
75        self.goal_symbol = elm;
76    }
77
78    /// Gets the goal symbol the lexer is currently using.
79    pub(crate) const fn get_goal(&self) -> InputElement {
80        self.goal_symbol
81    }
82
83    /// Returns if strict mode is currently active.
84    pub(super) const fn strict(&self) -> bool {
85        self.cursor.strict()
86    }
87
88    /// Sets the current strict mode.
89    pub(super) fn set_strict(&mut self, strict: bool) {
90        self.cursor.set_strict(strict);
91    }
92
93    /// Returns if module mode is currently active.
94    pub(super) const fn module(&self) -> bool {
95        self.cursor.module()
96    }
97
98    /// Signals that the goal symbol is a module
99    pub(super) fn set_module(&mut self, module: bool) {
100        self.cursor.set_module(module);
101    }
102
103    /// Creates a new lexer.
104    pub fn new(reader: R) -> Self
105    where
106        R: ReadChar,
107    {
108        Self {
109            cursor: Cursor::new(reader),
110            goal_symbol: InputElement::default(),
111        }
112    }
113
114    /// Handles lexing of a token starting '/' with the '/' already being consumed.
115    /// This could be a divide symbol or the start of a regex.
116    ///
117    /// If `init_with_eq` is `true`, assume that '/=' has already been consumed.
118    ///
119    /// A '/' symbol can always be a comment but if as tested above it is not then
120    /// that means it could be multiple different tokens depending on the input token.
121    ///
122    /// As per <https://tc39.es/ecma262/#sec-ecmascript-language-lexical-grammar>
123    pub(crate) fn lex_slash_token(
124        &mut self,
125        start: PositionGroup,
126        interner: &mut Interner,
127        init_with_eq: bool,
128    ) -> Result<Token, Error>
129    where
130        R: ReadChar,
131    {
132        if let Some(c) = self.cursor.peek_char()? {
133            match (c, init_with_eq) {
134                // /
135                (0x002F, false) => {
136                    self.cursor.next_char()?.expect("/ token vanished"); // Consume the '/'
137                    SingleLineComment.lex(&mut self.cursor, start, interner)
138                }
139                // *
140                (0x002A, false) => {
141                    self.cursor.next_char()?.expect("* token vanished"); // Consume the '*'
142                    MultiLineComment.lex(&mut self.cursor, start, interner)
143                }
144                (ch, init_with_eq) => {
145                    match self.get_goal() {
146                        InputElement::Div | InputElement::TemplateTail => {
147                            // Only div punctuator allowed, regex not.
148
149                            // =
150                            if init_with_eq || ch == 0x003D {
151                                // if `=` is not consumed, consume it
152                                if !init_with_eq {
153                                    // Indicates this is an AssignDiv.
154                                    // Consume the '='
155                                    self.cursor.next_char()?.expect("= token vanished");
156                                }
157                                Ok(Token::new_by_position_group(
158                                    Punctuator::AssignDiv.into(),
159                                    start,
160                                    self.cursor.pos_group(),
161                                ))
162                            } else {
163                                Ok(Token::new_by_position_group(
164                                    Punctuator::Div.into(),
165                                    start,
166                                    self.cursor.pos_group(),
167                                ))
168                            }
169                        }
170                        InputElement::RegExp | InputElement::HashbangOrRegExp => {
171                            // Can be a regular expression.
172                            RegexLiteral::new(init_with_eq).lex(&mut self.cursor, start, interner)
173                        }
174                    }
175                }
176            }
177        } else {
178            Ok(Token::new_by_position_group(
179                Punctuator::Div.into(),
180                start,
181                self.cursor.pos_group(),
182            ))
183        }
184    }
185
186    /// Skips an HTML close comment (`-->`) if the `annex-b` feature is enabled.
187    pub(crate) fn skip_html_close(&mut self, interner: &mut Interner) -> Result<(), Error>
188    where
189        R: ReadChar,
190    {
191        if cfg!(not(feature = "annex-b")) || self.module() {
192            return Ok(());
193        }
194
195        while self.cursor.peek_char()?.is_some_and(is_whitespace) {
196            let _next = self.cursor.next_char();
197        }
198
199        // -->
200        if self.cursor.peek_n(3)?[..3] == [Some(0x2D), Some(0x2D), Some(0x3E)] {
201            let _next = self.cursor.next_char();
202            let _next = self.cursor.next_char();
203            let _next = self.cursor.next_char();
204
205            let start = self.cursor.pos_group();
206            SingleLineComment.lex(&mut self.cursor, start, interner)?;
207        }
208
209        Ok(())
210    }
211
212    /// Retrieves the next token from the lexer.
213    ///
214    /// # Errors
215    ///
216    /// Will return `Err` on invalid tokens and invalid reads of the bytes being lexed.
217    // We intentionally don't implement Iterator trait as Result<Option> is cleaner to handle.
218    pub(crate) fn next_no_skip(&mut self, interner: &mut Interner) -> Result<Option<Token>, Error>
219    where
220        R: ReadChar,
221    {
222        let mut start = self.cursor.pos_group();
223        let Some(mut next_ch) = self.cursor.next_char()? else {
224            return Ok(None);
225        };
226
227        // If the goal symbol is HashbangOrRegExp, then we need to check if the next token is a hashbang comment.
228        // Since the goal symbol is only valid for the first token, we need to change it to RegExp after the first token.
229        if self.get_goal() == InputElement::HashbangOrRegExp {
230            self.set_goal(InputElement::RegExp);
231            if next_ch == 0x23 && self.cursor.peek_char()? == Some(0x21) {
232                let _token = HashbangComment.lex(&mut self.cursor, start, interner);
233                return self.next(interner);
234            }
235        }
236
237        // Ignore whitespace
238        if is_whitespace(next_ch) {
239            loop {
240                start = self.cursor.pos_group();
241                let Some(next) = self.cursor.next_char()? else {
242                    return Ok(None);
243                };
244                if !is_whitespace(next) {
245                    next_ch = next;
246                    break;
247                }
248            }
249        }
250
251        if let Ok(c) = char::try_from(next_ch) {
252            let token = match c {
253                '\r' | '\n' | '\u{2028}' | '\u{2029}' => Ok(Token::new_by_position_group(
254                    TokenKind::LineTerminator,
255                    start,
256                    self.cursor.pos_group(),
257                )),
258                '"' | '\'' => StringLiteral::new(c).lex(&mut self.cursor, start, interner),
259                '`' => TemplateLiteral.lex(&mut self.cursor, start, interner),
260                ';' => Ok(Token::new_by_position_group(
261                    Punctuator::Semicolon.into(),
262                    start,
263                    self.cursor.pos_group(),
264                )),
265                ':' => Ok(Token::new_by_position_group(
266                    Punctuator::Colon.into(),
267                    start,
268                    self.cursor.pos_group(),
269                )),
270                '.' => {
271                    if self
272                        .cursor
273                        .peek_char()?
274                        .filter(|c| (0x30..=0x39/* 0..=9 */).contains(c))
275                        .is_some()
276                    {
277                        NumberLiteral::new(b'.').lex(&mut self.cursor, start, interner)
278                    } else {
279                        SpreadLiteral::new().lex(&mut self.cursor, start, interner)
280                    }
281                }
282                '(' => Ok(Token::new_by_position_group(
283                    Punctuator::OpenParen.into(),
284                    start,
285                    self.cursor.pos_group(),
286                )),
287                ')' => Ok(Token::new_by_position_group(
288                    Punctuator::CloseParen.into(),
289                    start,
290                    self.cursor.pos_group(),
291                )),
292                ',' => Ok(Token::new_by_position_group(
293                    Punctuator::Comma.into(),
294                    start,
295                    self.cursor.pos_group(),
296                )),
297                '{' => Ok(Token::new_by_position_group(
298                    Punctuator::OpenBlock.into(),
299                    start,
300                    self.cursor.pos_group(),
301                )),
302                '}' => Ok(Token::new_by_position_group(
303                    Punctuator::CloseBlock.into(),
304                    start,
305                    self.cursor.pos_group(),
306                )),
307                '[' => Ok(Token::new_by_position_group(
308                    Punctuator::OpenBracket.into(),
309                    start,
310                    self.cursor.pos_group(),
311                )),
312                ']' => Ok(Token::new_by_position_group(
313                    Punctuator::CloseBracket.into(),
314                    start,
315                    self.cursor.pos_group(),
316                )),
317                '#' => PrivateIdentifier::new().lex(&mut self.cursor, start, interner),
318                '/' => self.lex_slash_token(start, interner, false),
319                #[cfg(feature = "annex-b")]
320                // <!--
321                '<' if !self.module()
322                    && self.cursor.peek_n(3)?[..3] == [Some(0x21), Some(0x2D), Some(0x2D)] =>
323                {
324                    let _next = self.cursor.next_char();
325                    let _next = self.cursor.next_char();
326                    let _next = self.cursor.next_char();
327                    let start = self.cursor.pos_group();
328                    SingleLineComment.lex(&mut self.cursor, start, interner)
329                }
330                #[allow(clippy::cast_possible_truncation)]
331                '=' | '*' | '+' | '-' | '%' | '|' | '&' | '^' | '<' | '>' | '!' | '~' | '?' => {
332                    Operator::new(next_ch as u8).lex(&mut self.cursor, start, interner)
333                }
334                '\\' if self.cursor.peek_char()? == Some(0x0075 /* u */) => {
335                    Identifier::new(c).lex(&mut self.cursor, start, interner)
336                }
337                _ if Identifier::is_identifier_start(c as u32) => {
338                    Identifier::new(c).lex(&mut self.cursor, start, interner)
339                }
340                #[allow(clippy::cast_possible_truncation)]
341                _ if c.is_ascii_digit() => {
342                    NumberLiteral::new(next_ch as u8).lex(&mut self.cursor, start, interner)
343                }
344                _ => {
345                    let details = format!(
346                        "unexpected '{c}' at line {}, column {}",
347                        start.line_number(),
348                        start.column_number()
349                    );
350                    Err(Error::syntax(details, start.position()))
351                }
352            }?;
353
354            Ok(Some(token))
355        } else {
356            Err(Error::syntax(
357                format!(
358                    "unexpected utf-8 char '\\u{next_ch}' at line {}, column {}",
359                    start.line_number(),
360                    start.column_number()
361                ),
362                start.position(),
363            ))
364        }
365    }
366
367    /// Retrieves the next token from the lexer, skipping comments.
368    ///
369    /// # Errors
370    ///
371    /// Will return `Err` on invalid tokens and invalid reads of the bytes being lexed.
372    // We intentionally don't implement Iterator trait as Result<Option> is cleaner to handle.
373    #[allow(clippy::should_implement_trait)]
374    pub fn next(&mut self, interner: &mut Interner) -> Result<Option<Token>, Error>
375    where
376        R: ReadChar,
377    {
378        loop {
379            let Some(next) = self.next_no_skip(interner)? else {
380                return Ok(None);
381            };
382
383            if next.kind() != &TokenKind::Comment {
384                return Ok(Some(next));
385            }
386        }
387    }
388
389    /// Performs the lexing of a template literal.
390    pub(crate) fn lex_template(
391        &mut self,
392        start: PositionGroup,
393        interner: &mut Interner,
394    ) -> Result<Token, Error>
395    where
396        R: ReadChar,
397    {
398        TemplateLiteral.lex(&mut self.cursor, start, interner)
399    }
400
401    pub(super) fn take_source(&mut self) -> boa_ast::SourceText {
402        self.cursor.take_source()
403    }
404}
405
406impl<'a> From<&'a [u8]> for Lexer<UTF8Input<&'a [u8]>> {
407    fn from(input: &'a [u8]) -> Self {
408        Self::new(UTF8Input::new(input))
409    }
410}
411
412/// ECMAScript goal symbols.
413///
414/// <https://tc39.es/ecma262/#sec-ecmascript-language-lexical-grammar>
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub(crate) enum InputElement {
417    Div,
418    RegExp,
419    TemplateTail,
420    HashbangOrRegExp,
421}
422
423impl Default for InputElement {
424    fn default() -> Self {
425        Self::RegExp
426    }
427}
428
429/// Checks if a character is whitespace as per ECMAScript standards.
430///
431/// The Rust `char::is_whitespace` function and the ECMAScript standard use different sets of
432/// characters as whitespaces:
433///  * Rust uses `\p{White_Space}`,
434///  * ECMAScript standard uses `\{Space_Separator}` + `\u{0009}`, `\u{000B}`, `\u{000C}`, `\u{FEFF}`
435///
436/// [More information](https://tc39.es/ecma262/#table-32)
437const fn is_whitespace(ch: u32) -> bool {
438    matches!(
439        ch,
440        0x0020 | 0x0009 | 0x000B | 0x000C | 0x00A0 | 0xFEFF |
441            // Unicode Space_Seperator category (minus \u{0020} and \u{00A0} which are allready stated above)
442            0x1680 | 0x2000..=0x200A | 0x202F | 0x205F | 0x3000
443    )
444}