1pub 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 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#[derive(Debug)]
67pub struct Lexer<R> {
68 cursor: Cursor<R>,
69 goal_symbol: InputElement,
70}
71
72impl<R> Lexer<R> {
73 pub(crate) fn set_goal(&mut self, elm: InputElement) {
75 self.goal_symbol = elm;
76 }
77
78 pub(crate) const fn get_goal(&self) -> InputElement {
80 self.goal_symbol
81 }
82
83 pub(super) const fn strict(&self) -> bool {
85 self.cursor.strict()
86 }
87
88 pub(super) fn set_strict(&mut self, strict: bool) {
90 self.cursor.set_strict(strict);
91 }
92
93 pub(super) const fn module(&self) -> bool {
95 self.cursor.module()
96 }
97
98 pub(super) fn set_module(&mut self, module: bool) {
100 self.cursor.set_module(module);
101 }
102
103 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 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 (0x002F, false) => {
136 self.cursor.next_char()?.expect("/ token vanished"); SingleLineComment.lex(&mut self.cursor, start, interner)
138 }
139 (0x002A, false) => {
141 self.cursor.next_char()?.expect("* token vanished"); MultiLineComment.lex(&mut self.cursor, start, interner)
143 }
144 (ch, init_with_eq) => {
145 match self.get_goal() {
146 InputElement::Div | InputElement::TemplateTail => {
147 if init_with_eq || ch == 0x003D {
151 if !init_with_eq {
153 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 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 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 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 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 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 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).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 '<' 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 ) => {
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 #[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 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#[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
429const fn is_whitespace(ch: u32) -> bool {
438 matches!(
439 ch,
440 0x0020 | 0x0009 | 0x000B | 0x000C | 0x00A0 | 0xFEFF |
441 0x1680 | 0x2000..=0x200A | 0x202F | 0x205F | 0x3000
443 )
444}